From 37a3bd697ae002cd7e6cca3d9dba6d45d92a0dd6 Mon Sep 17 00:00:00 2001 From: caballeto Date: Sun, 19 Apr 2026 13:22:02 +0200 Subject: [PATCH 01/10] refactor: use shared @devhelm/openapi-tools preprocessor Replace Python preprocess_spec.py with call to shared Node.js CLI tool. Fixes missing setRequiredOnAllOfMembers and flattenCircularOneOf steps, and removes divergent _REQUEST_PREFIXES exemption logic. Made-with: Cursor --- scripts/preprocess_spec.py | 106 ------- scripts/typegen.sh | 27 +- src/devhelm/_generated.py | 581 ++++++++++++++++++++----------------- 3 files changed, 332 insertions(+), 382 deletions(-) delete mode 100644 scripts/preprocess_spec.py diff --git a/scripts/preprocess_spec.py b/scripts/preprocess_spec.py deleted file mode 100644 index 746db51..0000000 --- a/scripts/preprocess_spec.py +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env python3 -"""Preprocess the vendored OpenAPI spec before running datamodel-codegen. - -Applies structural fixes that code generators need: -1. setRequiredFields — mark non-nullable fields as required -2. pushRequiredIntoAllOf — propagate required into allOf members -""" - -from __future__ import annotations - -import json -import sys -from pathlib import Path - - -_REQUEST_PREFIXES = ( - "Create", "Update", "Add", "Acquire", "Resolve", "Reorder", - "Test", "Change", "Admin", "Bulk", "Monitor", -) - - -def set_required_fields(spec: dict) -> None: - schemas = spec.get("components", {}).get("schemas", {}) - for schema_name, schema in schemas.items(): - if schema.get("type") != "object" or "properties" not in schema: - continue - - if isinstance(schema.get("required"), list): - is_req = schema_name.endswith("Request") and schema_name.startswith( - _REQUEST_PREFIXES - ) - if not is_req: - for prop, prop_schema in schema.get("properties", {}).items(): - if prop_schema.get("nullable"): - continue - if prop in schema["required"]: - continue - if prop_schema.get("allOf"): - continue - if "default" in prop_schema: - continue - schema["required"].append(prop) - continue - - is_request = schema_name.endswith("Request") and schema_name.startswith( - _REQUEST_PREFIXES - ) - if is_request: - continue - - required = [] - for prop, prop_schema in schema.get("properties", {}).items(): - if prop_schema.get("nullable"): - continue - if prop_schema.get("allOf"): - continue - if "default" in prop_schema: - continue - required.append(prop) - if required: - schema["required"] = required - - -def push_required_into_all_of(spec: dict) -> None: - schemas = spec.get("components", {}).get("schemas", {}) - for schema in schemas.values(): - if not isinstance(schema.get("required"), list): - continue - if not isinstance(schema.get("allOf"), list): - continue - for member in schema["allOf"]: - if "properties" not in member: - continue - member_required = [f for f in schema["required"] if f in member["properties"]] - if member_required: - existing = member.get("required", []) - member["required"] = list(set(existing + member_required)) - - - -# fix_missing_nullable — REMOVED. -# The root cause (Lombok not copying @Nullable to getters) was fixed in the API -# by adding `jakarta.annotation.Nullable` to lombok.copyableAnnotations. The -# generated OpenAPI spec now correctly marks nullable fields via the existing -# PropertyCustomizer in OpenApiConfig.java. All DTO fields also have explicit -# @Nullable or @NotNull/@NotBlank annotations, enforced by DtoAnnotationTest. - - -def main() -> None: - if len(sys.argv) != 3: - print(f"Usage: {sys.argv[0]} ", file=sys.stderr) - sys.exit(1) - - input_path = Path(sys.argv[1]) - output_path = Path(sys.argv[2]) - - spec = json.loads(input_path.read_text()) - set_required_fields(spec) - push_required_into_all_of(spec) - - output_path.write_text(json.dumps(spec, indent=2)) - print(f"Preprocessed: {input_path} -> {output_path}") - - -if __name__ == "__main__": - main() diff --git a/scripts/typegen.sh b/scripts/typegen.sh index 8464f45..dd398f4 100755 --- a/scripts/typegen.sh +++ b/scripts/typegen.sh @@ -2,6 +2,14 @@ # # Regenerate Pydantic models from the vendored OpenAPI spec. # +# Uses @devhelm/openapi-tools for preprocessing (shared with all surfaces), +# then runs datamodel-codegen for Pydantic model generation. +# +# Preprocessing resolution order: +# 1. $OPENAPI_TOOLS env var (explicit override) +# 2. Local monorepo sibling (../mini/packages/openapi-tools) +# 3. npx from npm (CI / standalone) +# set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -16,8 +24,23 @@ if [[ ! -f "$INPUT" ]]; then exit 1 fi -echo "=> Preprocessing OpenAPI spec..." -python3 "$SCRIPT_DIR/preprocess_spec.py" "$INPUT" "$PREPROCESSED" +resolve_openapi_tools() { + if [[ -n "${OPENAPI_TOOLS:-}" ]]; then + echo "$OPENAPI_TOOLS" + return + fi + local local_cli="$ROOT_DIR/../mini/packages/openapi-tools/dist/cli.js" + if [[ -f "$local_cli" ]]; then + echo "node $local_cli" + return + fi + echo "npx --yes --package=@devhelm/openapi-tools devhelm-openapi" +} + +TOOLS_CMD=$(resolve_openapi_tools) + +echo "=> Preprocessing OpenAPI spec (via @devhelm/openapi-tools)..." +$TOOLS_CMD preprocess "$INPUT" "$PREPROCESSED" echo "=> Generating Pydantic models from preprocessed spec..." diff --git a/src/devhelm/_generated.py b/src/devhelm/_generated.py index 7f09059..81d8417 100644 --- a/src/devhelm/_generated.py +++ b/src/devhelm/_generated.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: .openapi-preprocessed.json -# timestamp: 2026-04-18T23:23:30+00:00 +# timestamp: 2026-04-19T10:56:52+00:00 from __future__ import annotations from typing import Annotated, Any @@ -242,22 +242,23 @@ class AlertDeliveryDto(BaseModel): ), ] step_number: Annotated[ - int, + int | None, Field( alias="stepNumber", description="1-based escalation step this delivery belongs to", ), - ] + ] = None fire_count: Annotated[ - int, + int | None, Field( alias="fireCount", description="Fire sequence within the step: 1 = initial, 2+ = repeat re-fires", ), - ] + ] = None attempt_count: Annotated[ - int, Field(alias="attemptCount", description="Number of delivery attempts made") - ] + int | None, + Field(alias="attemptCount", description="Number of delivery attempts made"), + ] = None last_attempt_at: Annotated[ AwareDatetime | None, Field(alias="lastAttemptAt", description="When the last attempt was made"), @@ -287,7 +288,7 @@ class AlertDeliveryDto(BaseModel): class ApiKeyCreateResponse(BaseModel): - id: Annotated[int, Field(description="Unique API key identifier")] + id: Annotated[int | None, Field(description="Unique API key identifier")] = None name: Annotated[ str, Field(description="Human-readable name for this API key", min_length=1) ] @@ -312,7 +313,7 @@ class ApiKeyCreateResponse(BaseModel): class ApiKeyDto(BaseModel): - id: Annotated[int, Field(description="Unique API key identifier")] + id: Annotated[int | None, Field(description="Unique API key identifier")] = None name: Annotated[ str, Field(description="Human-readable name for this API key", min_length=1) ] @@ -361,7 +362,9 @@ class Severity(StrEnum): class AssertionResultDto(BaseModel): type: Annotated[str, Field(description="Assertion type", examples=["status_code"])] - passed: Annotated[bool, Field(description="Whether the assertion passed")] + passed: Annotated[ + bool | None, Field(description="Whether the assertion passed") + ] = None severity: Annotated[Severity, Field(description="Assertion severity")] message: Annotated[ str | None, Field(description="Human-readable result message") @@ -424,7 +427,9 @@ class AssertionTestResultDto(BaseModel): AssertionType, Field(alias="assertionType", description="Assertion type evaluated"), ] - passed: Annotated[bool, Field(description="Whether the assertion passed")] + passed: Annotated[ + bool | None, Field(description="Whether the assertion passed") + ] = None severity: Annotated[Severity, Field(description="Assertion severity: FAIL or WARN")] message: Annotated[str, Field(description="Human-readable result description")] expected: Annotated[str | None, Field(description="Expected value")] = None @@ -434,7 +439,7 @@ class AssertionTestResultDto(BaseModel): class AuditEventDto(BaseModel): - id: Annotated[int, Field(description="Unique audit event identifier")] + id: Annotated[int | None, Field(description="Unique audit event identifier")] = None actor_id: Annotated[ int | None, Field( @@ -502,9 +507,9 @@ class CategoryDto(BaseModel): str, Field(description="Category name (e.g. CI/CD, Cloud, Payments)") ] service_count: Annotated[ - int, + int | None, Field(alias="serviceCount", description="Number of services in this category"), - ] + ] = None class OrgRole(StrEnum): @@ -599,26 +604,26 @@ class ComponentImpact(BaseModel): ), ] = None uptime_percentage: Annotated[ - float, + float | None, Field( alias="uptimePercentage", description="Computed uptime % for this component on this day", ), - ] + ] = None partial_outage_seconds: Annotated[ - int, + int | None, Field( alias="partialOutageSeconds", description="Seconds of partial outage observed on this day", ), - ] + ] = None major_outage_seconds: Annotated[ - int, + int | None, Field( alias="majorOutageSeconds", description="Seconds of major outage observed on this day", ), - ] + ] = None class ComponentPosition(BaseModel): @@ -626,8 +631,9 @@ class ComponentPosition(BaseModel): UUID, Field(alias="componentId", description="Component ID") ] display_order: Annotated[ - int, Field(alias="displayOrder", description="New display order (0-based)") - ] + int | None, + Field(alias="displayOrder", description="New display order (0-based)"), + ] = None group_id: Annotated[ UUID | None, Field(alias="groupId", description="Target group ID, null for ungrouped"), @@ -677,19 +683,19 @@ class ConfirmationPolicy(BaseModel): Field(description="How incident confirmation is coordinated across regions"), ] min_regions_failing: Annotated[ - int, + int | None, Field( alias="minRegionsFailing", description="Minimum failing regions required to confirm an incident", ), - ] + ] = None max_wait_seconds: Annotated[ - int, + int | None, Field( alias="maxWaitSeconds", description="Maximum seconds to wait for enough regions to fail after first trigger", ), - ] + ] = None class CreateApiKeyRequest(BaseModel): @@ -1110,11 +1116,11 @@ class CursorPage(BaseModel): ), ] = None has_more: Annotated[ - bool, + bool | None, Field( alias="hasMore", description="Whether more results exist beyond this page" ), - ] + ] = None class DayIncident(BaseModel): @@ -1130,11 +1136,11 @@ class DayIncident(BaseModel): Impact, Field(description="Severity bucket (none, minor, major, critical)") ] scheduled: Annotated[ - bool, + bool | None, Field( description="True for scheduled maintenances; false for unplanned incidents" ), - ] + ] = None started_at: Annotated[ AwareDatetime | None, Field(alias="startedAt", description="Incident start timestamp"), @@ -1157,26 +1163,27 @@ class DayIncident(BaseModel): class DekRotationResultDto(BaseModel): previous_dek_version: Annotated[ - int, + int | None, Field(alias="previousDekVersion", description="DEK version before rotation"), - ] + ] = None new_dek_version: Annotated[ - int, Field(alias="newDekVersion", description="DEK version after rotation") - ] + int | None, + Field(alias="newDekVersion", description="DEK version after rotation"), + ] = None secrets_re_encrypted: Annotated[ - int, + int | None, Field( alias="secretsReEncrypted", description="Number of secrets re-encrypted with the new DEK", ), - ] + ] = None channels_re_encrypted: Annotated[ - int, + int | None, Field( alias="channelsReEncrypted", description="Number of alert channels re-encrypted with the new DEK", ), - ] + ] = None rotated_at: Annotated[ AwareDatetime, Field( @@ -1206,8 +1213,8 @@ class DeliveryAttemptDto(BaseModel): id: UUID delivery_id: Annotated[UUID, Field(alias="deliveryId")] attempt_number: Annotated[ - int, Field(alias="attemptNumber", description="1-based attempt number") - ] + int | None, Field(alias="attemptNumber", description="1-based attempt number") + ] = None status: Annotated[ str, Field(description="Outcome: SUCCESS, FAILED, TIMEOUT, ERROR") ] @@ -1452,25 +1459,26 @@ class EmailChannelConfig(ChannelConfig): class EntitlementDto(BaseModel): key: Annotated[str, Field(description="Entitlement key")] value: Annotated[ - int, Field(description="Effective limit value (overrides applied)") - ] + int | None, Field(description="Effective limit value (overrides applied)") + ] = None default_value: Annotated[ - int, + int | None, Field( alias="defaultValue", description="Plan-tier default value before overrides" ), - ] + ] = None overridden: Annotated[ - bool, Field(description="Whether this entitlement has an org-level override") - ] + bool | None, + Field(description="Whether this entitlement has an org-level override"), + ] = None class EnvironmentDto(BaseModel): id: Annotated[UUID, Field(description="Unique environment identifier")] org_id: Annotated[ - int, + int | None, Field(alias="orgId", description="Organization this environment belongs to"), - ] + ] = None name: Annotated[ str, Field(description="Human-readable environment name", min_length=1) ] @@ -1493,30 +1501,30 @@ class EnvironmentDto(BaseModel): ), ] monitor_count: Annotated[ - int, + int | None, Field( alias="monitorCount", description="Number of monitors using this environment", ), - ] + ] = None is_default: Annotated[ - bool, + bool | None, Field( alias="isDefault", description="Whether this is the default environment for new monitors", ), - ] + ] = None class EscalationStep(BaseModel): delay_minutes: Annotated[ - int, + int | None, Field( alias="delayMinutes", description="Minutes to wait before executing this step (0 = immediate)", ge=0, ), - ] + ] = None channel_ids: Annotated[ list[UUID], Field( @@ -1707,11 +1715,11 @@ class IncidentDto(BaseModel): ), ] = None organization_id: Annotated[ - int, + int | None, Field( alias="organizationId", description="Organization this incident belongs to" ), - ] + ] = None source: Annotated[ Source, Field(description="Incident origin: MONITOR, SERVICE, or MANUAL") ] @@ -1742,12 +1750,12 @@ class IncidentDto(BaseModel): ), ] reopen_count: Annotated[ - int, + int | None, Field( alias="reopenCount", description="Number of times this incident has been reopened", ), - ] + ] = None created_by_user_id: Annotated[ int | None, Field( @@ -1756,12 +1764,12 @@ class IncidentDto(BaseModel): ), ] = None status_page_visible: Annotated[ - bool, + bool | None, Field( alias="statusPageVisible", description="Whether this incident is visible on the status page", ), - ] + ] = None service_incident_id: Annotated[ UUID | None, Field( @@ -1931,7 +1939,7 @@ class IncidentUpdateDto(BaseModel): new_status: Annotated[NewStatus | None, Field(alias="newStatus")] = None body: str | None = None created_by: Annotated[CreatedBy | None, Field(alias="createdBy")] = None - notify_subscribers: Annotated[bool, Field(alias="notifySubscribers")] + notify_subscribers: Annotated[bool | None, Field(alias="notifySubscribers")] = None created_at: Annotated[AwareDatetime, Field(alias="createdAt")] @@ -1958,8 +1966,8 @@ class IntegrationFieldDto(BaseModel): class InviteDto(BaseModel): invite_id: Annotated[ - int, Field(alias="inviteId", description="Unique invite identifier") - ] + int | None, Field(alias="inviteId", description="Unique invite identifier") + ] = None email: Annotated[str, Field(description="Email address the invite was sent to")] role_offered: Annotated[ RoleOffered, @@ -2008,7 +2016,7 @@ class JsonPathAssertion(AssertionConfig): class KeyInfo(BaseModel): - id: Annotated[int, Field(description="Key ID")] + id: Annotated[int | None, Field(description="Key ID")] = None name: Annotated[str, Field(description="Human-readable key name")] created_at: Annotated[ AwareDatetime, Field(alias="createdAt", description="When the key was created") @@ -2038,7 +2046,7 @@ class LinkedStatusPageIncidentDto(BaseModel): title: str status: Status8 impact: Impact - scheduled: bool + scheduled: bool | None = None published_at: Annotated[AwareDatetime | None, Field(alias="publishedAt")] = None @@ -2072,12 +2080,12 @@ class MaintenanceWindowDto(BaseModel): ), ] = None organization_id: Annotated[ - int, + int | None, Field( alias="organizationId", description="Organization this maintenance window belongs to", ), - ] + ] = None starts_at: Annotated[ AwareDatetime, Field( @@ -2099,12 +2107,12 @@ class MaintenanceWindowDto(BaseModel): str | None, Field(description="Human-readable reason for the maintenance") ] = None suppress_alerts: Annotated[ - bool, + bool | None, Field( alias="suppressAlerts", description="Whether alerts are suppressed during this window", ), - ] + ] = None created_at: Annotated[ AwareDatetime, Field(alias="createdAt", description="Timestamp when the window was created"), @@ -2217,8 +2225,8 @@ class Status9(StrEnum): class MemberDto(BaseModel): user_id: Annotated[ - int, Field(alias="userId", description="User identifier of the member") - ] + int | None, Field(alias="userId", description="User identifier of the member") + ] = None email: Annotated[str, Field(description="Member email address")] name: Annotated[ str | None, Field(description="Member display name; null if not set") @@ -2305,7 +2313,7 @@ class MonitorsSummaryDto(BaseModel): class MonitorTestResultDto(BaseModel): - passed: bool + passed: bool | None = None error: str | None = None status_code: Annotated[int | None, Field(alias="statusCode")] = None response_time_ms: Annotated[int | None, Field(alias="responseTimeMs")] = None @@ -2383,12 +2391,12 @@ class NotificationDispatchDto(BaseModel): ), ] = None current_step: Annotated[ - int, + int | None, Field( alias="currentStep", description="1-based index of the currently active escalation step", ), - ] + ] = None total_steps: Annotated[ int | None, Field( @@ -2437,7 +2445,9 @@ class NotificationDispatchDto(BaseModel): class NotificationDto(BaseModel): - id: Annotated[int, Field(description="Unique notification identifier")] + id: Annotated[int | None, Field(description="Unique notification identifier")] = ( + None + ) type: Annotated[ str, Field(description="Notification category (e.g. incident, monitor, team)") ] @@ -2460,7 +2470,9 @@ class NotificationDto(BaseModel): description="ID of the resource this notification is about", ), ] = None - read: Annotated[bool, Field(description="Whether the notification has been read")] + read: Annotated[ + bool | None, Field(description="Whether the notification has been read") + ] = None created_at: Annotated[ AwareDatetime, Field( @@ -2484,7 +2496,9 @@ class OpsGenieChannelConfig(ChannelConfig): class OrganizationDto(BaseModel): - id: Annotated[int, Field(description="Unique organization identifier")] + id: Annotated[int | None, Field(description="Unique organization identifier")] = ( + None + ) name: Annotated[str, Field(description="Organization name")] email: Annotated[str, Field(description="Billing and contact email")] size: Annotated[ @@ -2499,7 +2513,7 @@ class OrganizationDto(BaseModel): class OrgInfo(BaseModel): - id: Annotated[int, Field(description="Organization ID")] + id: Annotated[int | None, Field(description="Organization ID")] = None name: Annotated[str, Field(description="Organization name")] @@ -2562,8 +2576,9 @@ class PlanInfo(BaseModel): ), ] = None trial_active: Annotated[ - bool, Field(alias="trialActive", description="Whether the org is on a trial") - ] + bool | None, + Field(alias="trialActive", description="Whether the org is on a trial"), + ] = None trial_expires_at: Annotated[ AwareDatetime | None, Field( @@ -2601,11 +2616,11 @@ class PollChartBucketDto(BaseModel): ), ] = None total_polls: Annotated[ - int, + int | None, Field( alias="totalPolls", description="Total polls in this bucket", examples=[60] ), - ] + ] = None class Status11(StrEnum): @@ -2726,11 +2741,11 @@ class RegexBodyAssertion(AssertionConfig): class RegionStatusDto(BaseModel): region: Annotated[str, Field(description="Region identifier", examples=["us-east"])] passed: Annotated[ - bool, + bool | None, Field( description="Whether the last check in this region passed", examples=[True] ), - ] + ] = None response_time_ms: Annotated[ int | None, Field( @@ -2811,23 +2826,23 @@ class ResourceGroupHealthDto(BaseModel): Status12, Field(description="Worst-of health status across all members") ] total_members: Annotated[ - int, + int | None, Field(alias="totalMembers", description="Total number of members in the group"), - ] + ] = None operational_count: Annotated[ - int, + int | None, Field( alias="operationalCount", description="Number of members currently in operational status", ), - ] + ] = None active_incidents: Annotated[ - int, + int | None, Field( alias="activeIncidents", description="Number of members with an active incident or non-operational status", ), - ] + ] = None threshold_status: Annotated[ ThresholdStatus | None, Field( @@ -3026,15 +3041,15 @@ class RetryStrategy(BaseModel): Field(description="Retry strategy kind, e.g. fixed interval between attempts"), ] max_retries: Annotated[ - int, + int | None, Field( alias="maxRetries", description="Maximum number of retries after a failed check", ), - ] + ] = None interval: Annotated[ - int, Field(description="Delay between retry attempts in seconds") - ] + int | None, Field(description="Delay between retry attempts in seconds") + ] = None class ScheduledMaintenanceDto(BaseModel): @@ -3104,11 +3119,11 @@ class SecretDto(BaseModel): str, Field(description="Secret key name, unique within the workspace") ] dek_version: Annotated[ - int, + int | None, Field( alias="dekVersion", description="DEK version at the time of last encryption" ), - ] + ] = None value_hash: Annotated[ str, Field( @@ -3161,14 +3176,18 @@ class ServiceCatalogDto(BaseModel): developer_context: Annotated[str | None, Field(alias="developerContext")] = None logo_url: Annotated[str | None, Field(alias="logoUrl")] = None adapter_type: Annotated[str, Field(alias="adapterType")] - polling_interval_seconds: Annotated[int, Field(alias="pollingIntervalSeconds")] - enabled: bool - published: bool + polling_interval_seconds: Annotated[ + int | None, Field(alias="pollingIntervalSeconds") + ] = None + enabled: bool | None = None + published: bool | None = None overall_status: Annotated[str | None, Field(alias="overallStatus")] = None created_at: Annotated[AwareDatetime, Field(alias="createdAt")] updated_at: Annotated[AwareDatetime, Field(alias="updatedAt")] - component_count: Annotated[int, Field(alias="componentCount")] - active_incident_count: Annotated[int, Field(alias="activeIncidentCount")] + component_count: Annotated[int | None, Field(alias="componentCount")] = None + active_incident_count: Annotated[int | None, Field(alias="activeIncidentCount")] = ( + None + ) data_completeness: Annotated[str, Field(alias="dataCompleteness")] uptime30d: Annotated[ float | None, @@ -3184,8 +3203,10 @@ class ServiceComponentDto(BaseModel): description: str | None = None group_id: Annotated[UUID | None, Field(alias="groupId")] = None position: int | None = None - showcase: bool - only_show_if_degraded: Annotated[bool, Field(alias="onlyShowIfDegraded")] + showcase: bool | None = None + only_show_if_degraded: Annotated[bool | None, Field(alias="onlyShowIfDegraded")] = ( + None + ) start_date: Annotated[AwareDatetime | None, Field(alias="startDate")] = None vendor_created_at: Annotated[ AwareDatetime | None, Field(alias="vendorCreatedAt") @@ -3200,12 +3221,12 @@ class ServiceComponentDto(BaseModel): ), ] has_uptime: Annotated[ - bool, + bool | None, Field( alias="hasUptime", description="Whether uptime data is available for this component", ), - ] + ] = None region: Annotated[ str | None, Field( @@ -3217,12 +3238,12 @@ class ServiceComponentDto(BaseModel): Field(alias="groupName", description="Display name of the parent group"), ] = None display_aggregated_uptime: Annotated[ - bool, + bool | None, Field( alias="displayAggregatedUptime", description="Group-only: render an aggregated uptime bar above this group's children", ), - ] + ] = None child_count: Annotated[ int | None, Field( @@ -3236,7 +3257,7 @@ class ServiceComponentDto(BaseModel): ] = None first_seen_at: Annotated[AwareDatetime, Field(alias="firstSeenAt")] last_seen_at: Annotated[AwareDatetime, Field(alias="lastSeenAt")] - is_group: Annotated[bool, Field(alias="isGroup")] + is_group: Annotated[bool | None, Field(alias="isGroup")] = None class ServiceDayDetailDto(BaseModel): @@ -3251,19 +3272,19 @@ class ServiceDayDetailDto(BaseModel): ), ] = None total_partial_outage_seconds: Annotated[ - int, + int | None, Field( alias="totalPartialOutageSeconds", description="Sum of partial outage seconds across all leaf components", ), - ] + ] = None total_major_outage_seconds: Annotated[ - int, + int | None, Field( alias="totalMajorOutageSeconds", description="Sum of major outage seconds across all leaf components", ), - ] + ] = None components: Annotated[ list[ComponentImpact], Field( @@ -3319,12 +3340,12 @@ class ServiceLiveStatusDto(BaseModel): ), ] active_incident_count: Annotated[ - int, + int | None, Field( alias="activeIncidentCount", description="Number of currently unresolved incidents", ), - ] + ] = None last_polled_at: Annotated[ str | None, Field( @@ -3365,8 +3386,8 @@ class ServicePollResultDto(BaseModel): ), ] = None passed: Annotated[ - bool, Field(description="Whether the poll succeeded", examples=[True]) - ] + bool | None, Field(description="Whether the poll succeeded", examples=[True]) + ] = None failure_reason: Annotated[ str | None, Field( @@ -3374,21 +3395,21 @@ class ServicePollResultDto(BaseModel): ), ] = None component_count: Annotated[ - int, + int | None, Field( alias="componentCount", description="Number of components reported by the service", examples=[12], ), - ] + ] = None degraded_count: Annotated[ - int, + int | None, Field( alias="degradedCount", description="Number of degraded or non-operational components", examples=[1], ), - ] + ] = None class ServicePollSummaryDto(BaseModel): @@ -3401,21 +3422,21 @@ class ServicePollSummaryDto(BaseModel): ), ] = None total_polls: Annotated[ - int, + int | None, Field( alias="totalPolls", description="Total number of polls executed", examples=[4320], ), - ] + ] = None passed_polls: Annotated[ - int, + int | None, Field( alias="passedPolls", description="Number of polls that succeeded", examples=[4318], ), - ] + ] = None avg_response_time_ms: Annotated[ float | None, Field( @@ -3485,8 +3506,10 @@ class ServiceSubscriptionDto(BaseModel): category: str | None = None official_status_url: Annotated[str | None, Field(alias="officialStatusUrl")] = None adapter_type: Annotated[str, Field(alias="adapterType", min_length=1)] - polling_interval_seconds: Annotated[int, Field(alias="pollingIntervalSeconds")] - enabled: bool + polling_interval_seconds: Annotated[ + int | None, Field(alias="pollingIntervalSeconds") + ] = None + enabled: bool | None = None logo_url: Annotated[ str | None, Field(alias="logoUrl", description="Logo URL from the service catalog"), @@ -3815,10 +3838,12 @@ class StatusPageComponentDto(BaseModel): monitor_id: Annotated[UUID | None, Field(alias="monitorId")] = None resource_group_id: Annotated[UUID | None, Field(alias="resourceGroupId")] = None current_status: Annotated[CurrentStatus1, Field(alias="currentStatus")] - show_uptime: Annotated[bool, Field(alias="showUptime")] - display_order: Annotated[int, Field(alias="displayOrder")] - page_order: Annotated[int, Field(alias="pageOrder")] - exclude_from_overall: Annotated[bool, Field(alias="excludeFromOverall")] + show_uptime: Annotated[bool | None, Field(alias="showUptime")] = None + display_order: Annotated[int | None, Field(alias="displayOrder")] = None + page_order: Annotated[int | None, Field(alias="pageOrder")] = None + exclude_from_overall: Annotated[bool | None, Field(alias="excludeFromOverall")] = ( + None + ) start_date: Annotated[AwareDatetime | None, Field(alias="startDate")] = None created_at: Annotated[AwareDatetime, Field(alias="createdAt")] updated_at: Annotated[AwareDatetime, Field(alias="updatedAt")] @@ -3829,9 +3854,9 @@ class StatusPageComponentGroupDto(BaseModel): status_page_id: Annotated[UUID, Field(alias="statusPageId")] name: str description: str | None = None - display_order: Annotated[int, Field(alias="displayOrder")] - page_order: Annotated[int, Field(alias="pageOrder")] - collapsed: bool + display_order: Annotated[int | None, Field(alias="displayOrder")] = None + page_order: Annotated[int | None, Field(alias="pageOrder")] = None + collapsed: bool | None = None components: list[StatusPageComponentDto] | None = None created_at: Annotated[AwareDatetime, Field(alias="createdAt")] updated_at: Annotated[AwareDatetime, Field(alias="updatedAt")] @@ -3865,7 +3890,7 @@ class StatusPageCustomDomainDto(BaseModel): verification_error: Annotated[str | None, Field(alias="verificationError")] = None created_at: Annotated[AwareDatetime, Field(alias="createdAt")] updated_at: Annotated[AwareDatetime, Field(alias="updatedAt")] - primary: bool + primary: bool | None = None class OverallStatus(StrEnum): @@ -3878,14 +3903,14 @@ class OverallStatus(StrEnum): class StatusPageDto(BaseModel): id: UUID - organization_id: Annotated[int, Field(alias="organizationId")] - workspace_id: Annotated[int, Field(alias="workspaceId")] + organization_id: Annotated[int | None, Field(alias="organizationId")] = None + workspace_id: Annotated[int | None, Field(alias="workspaceId")] = None name: Annotated[str, Field(min_length=1)] slug: Annotated[str, Field(min_length=1)] description: str | None = None branding: StatusPageBranding visibility: Visibility - enabled: bool + enabled: bool | None = None incident_mode: Annotated[IncidentMode, Field(alias="incidentMode")] component_count: Annotated[int | None, Field(alias="componentCount")] = None subscriber_count: Annotated[int | None, Field(alias="subscriberCount")] = None @@ -3926,14 +3951,14 @@ class StatusPageIncidentUpdateDto(BaseModel): body: str created_by: Annotated[CreatedBy1 | None, Field(alias="createdBy")] = None created_by_user_id: Annotated[int | None, Field(alias="createdByUserId")] = None - notify_subscribers: Annotated[bool, Field(alias="notifySubscribers")] + notify_subscribers: Annotated[bool | None, Field(alias="notifySubscribers")] = None created_at: Annotated[AwareDatetime, Field(alias="createdAt")] class StatusPageSubscriberDto(BaseModel): id: UUID email: str - confirmed: bool + confirmed: bool | None = None created_at: Annotated[AwareDatetime, Field(alias="createdAt")] @@ -3945,184 +3970,184 @@ class Summary(BaseModel): class TableValueResultAlertChannelDto(BaseModel): data: list[AlertChannelDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultAlertDeliveryDto(BaseModel): data: list[AlertDeliveryDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultApiKeyDto(BaseModel): data: list[ApiKeyDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultAuditEventDto(BaseModel): data: list[AuditEventDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultCategoryDto(BaseModel): data: list[CategoryDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultDeliveryAttemptDto(BaseModel): data: list[DeliveryAttemptDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultEnvironmentDto(BaseModel): data: list[EnvironmentDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultIncidentDto(BaseModel): data: list[IncidentDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultInviteDto(BaseModel): data: list[InviteDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultMaintenanceWindowDto(BaseModel): data: list[MaintenanceWindowDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultMemberDto(BaseModel): data: list[MemberDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultNotificationDispatchDto(BaseModel): data: list[NotificationDispatchDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultNotificationDto(BaseModel): data: list[NotificationDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultScheduledMaintenanceDto(BaseModel): data: list[ScheduledMaintenanceDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultSecretDto(BaseModel): data: list[SecretDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultServiceComponentDto(BaseModel): data: list[ServiceComponentDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultServiceIncidentDto(BaseModel): data: list[ServiceIncidentDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultServiceSubscriptionDto(BaseModel): data: list[ServiceSubscriptionDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultStatusPageComponentDto(BaseModel): data: list[StatusPageComponentDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultStatusPageComponentGroupDto(BaseModel): data: list[StatusPageComponentGroupDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultStatusPageCustomDomainDto(BaseModel): data: list[StatusPageCustomDomainDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultStatusPageDto(BaseModel): data: list[StatusPageDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultStatusPageSubscriberDto(BaseModel): data: list[StatusPageSubscriberDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None @@ -4130,9 +4155,9 @@ class TableValueResultStatusPageSubscriberDto(BaseModel): class TagDto(BaseModel): id: Annotated[UUID, Field(description="Unique tag identifier")] organization_id: Annotated[ - int, + int | None, Field(alias="organizationId", description="Organization this tag belongs to"), - ] + ] = None name: Annotated[ str, Field(description="Tag name, unique within the org", min_length=1) ] @@ -4197,17 +4222,17 @@ class TeamsChannelConfig(ChannelConfig): class TestChannelResult(BaseModel): - success: bool + success: bool | None = None message: str class TestMatchResult(BaseModel): matched: Annotated[ - bool, + bool | None, Field( description="Whether the policy would match the supplied incident context" ), - ] + ] = None matched_rules: Annotated[ list[str], Field(alias="matchedRules", description="Rules that passed evaluation"), @@ -4929,13 +4954,13 @@ class UptimeBucketDto(BaseModel): ), ] = None total_polls: Annotated[ - int, + int | None, Field( alias="totalPolls", description="Total number of polls recorded in this bucket", examples=[12], ), - ] + ] = None class UptimeDto(BaseModel): @@ -5011,8 +5036,8 @@ class WebhookDeliveryDto(BaseModel): event_id: Annotated[str, Field(alias="eventId")] event_type: Annotated[str, Field(alias="eventType")] status: str - attempt_count: Annotated[int, Field(alias="attemptCount")] - max_attempts: Annotated[int, Field(alias="maxAttempts")] + attempt_count: Annotated[int | None, Field(alias="attemptCount")] = None + max_attempts: Annotated[int | None, Field(alias="maxAttempts")] = None response_status: Annotated[int | None, Field(alias="responseStatus")] = None response_latency_ms: Annotated[int | None, Field(alias="responseLatencyMs")] = None error_message: Annotated[str | None, Field(alias="errorMessage")] = None @@ -5038,15 +5063,15 @@ class WebhookEndpointDto(BaseModel): ), ] enabled: Annotated[ - bool, Field(description="Whether delivery is enabled for this endpoint") - ] + bool | None, Field(description="Whether delivery is enabled for this endpoint") + ] = None consecutive_failures: Annotated[ - int, + int | None, Field( alias="consecutiveFailures", description="Number of consecutive delivery failures", ), - ] + ] = None disabled_reason: Annotated[ str | None, Field( @@ -5102,14 +5127,14 @@ class WebhookSigningSecretDto(BaseModel): class WebhookTestResult(BaseModel): - success: bool + success: bool | None = None status_code: Annotated[int | None, Field(alias="statusCode")] = None message: str duration_ms: Annotated[int | None, Field(alias="durationMs")] = None class WorkspaceDto(BaseModel): - id: Annotated[int, Field(description="Unique workspace identifier")] + id: Annotated[int | None, Field(description="Unique workspace identifier")] = None created_at: Annotated[ AwareDatetime, Field( @@ -5125,8 +5150,9 @@ class WorkspaceDto(BaseModel): ] name: Annotated[str, Field(description="Workspace name", min_length=1)] org_id: Annotated[ - int, Field(alias="orgId", description="Organization this workspace belongs to") - ] + int | None, + Field(alias="orgId", description="Organization this workspace belongs to"), + ] = None class AddMonitorTagsRequest(BaseModel): @@ -5304,8 +5330,8 @@ class CheckResultDto(BaseModel): ), ] = None passed: Annotated[ - bool, Field(description="Whether the check passed", examples=[True]) - ] + bool | None, Field(description="Whether the check passed", examples=[True]) + ] = None failure_reason: Annotated[ str | None, Field( @@ -5335,26 +5361,26 @@ class ComponentUptimeDayDto(BaseModel): Field(description="Start-of-day timestamp for this bucket (UTC midnight)"), ] partial_outage_seconds: Annotated[ - int, + int | None, Field( alias="partialOutageSeconds", description="Seconds of partial outage on this day", ), - ] + ] = None major_outage_seconds: Annotated[ - int, + int | None, Field( alias="majorOutageSeconds", description="Seconds of major outage on this day", ), - ] + ] = None uptime_percentage: Annotated[ - float, + float | None, Field( alias="uptimePercentage", description="Computed uptime percentage using weighted formula", ), - ] + ] = None incidents: Annotated[ list[IncidentRef] | None, Field(description="Incidents that overlapped this day"), @@ -5582,11 +5608,11 @@ class CursorPageCheckResultDto(BaseModel): ), ] = None has_more: Annotated[ - bool, + bool | None, Field( alias="hasMore", description="Whether more results exist beyond this page" ), - ] + ] = None class CursorPageServiceCatalogDto(BaseModel): @@ -5599,11 +5625,11 @@ class CursorPageServiceCatalogDto(BaseModel): ), ] = None has_more: Annotated[ - bool, + bool | None, Field( alias="hasMore", description="Whether more results exist beyond this page" ), - ] + ] = None class CursorPageServicePollResultDto(BaseModel): @@ -5616,11 +5642,11 @@ class CursorPageServicePollResultDto(BaseModel): ), ] = None has_more: Annotated[ - bool, + bool | None, Field( alias="hasMore", description="Whether more results exist beyond this page" ), - ] + ] = None class DashboardOverviewDto(BaseModel): @@ -5675,58 +5701,58 @@ class EscalationChain(BaseModel): class GlobalStatusSummaryDto(BaseModel): total_services: Annotated[ - int, + int | None, Field( alias="totalServices", description="Total number of services in the catalog" ), - ] + ] = None operational_count: Annotated[ - int, + int | None, Field( alias="operationalCount", description="Number of services currently fully operational", ), - ] + ] = None degraded_count: Annotated[ - int, + int | None, Field( alias="degradedCount", description="Number of services with degraded status" ), - ] + ] = None partial_outage_count: Annotated[ - int, + int | None, Field( alias="partialOutageCount", description="Number of services with partial outage", ), - ] + ] = None major_outage_count: Annotated[ - int, + int | None, Field( alias="majorOutageCount", description="Number of services with major outage" ), - ] + ] = None maintenance_count: Annotated[ - int, + int | None, Field( alias="maintenanceCount", description="Number of services currently under maintenance", ), - ] + ] = None unknown_count: Annotated[ - int, + int | None, Field( alias="unknownCount", description="Number of services with unknown or null status", ), - ] + ] = None active_incident_count: Annotated[ - int, + int | None, Field( alias="activeIncidentCount", description="Total number of active incidents across all services", ), - ] + ] = None services_with_issues: Annotated[ list[ServiceCatalogDto], Field( @@ -5972,11 +5998,11 @@ class MonitorAuthDto(BaseModel): class MonitorDto(BaseModel): id: Annotated[UUID, Field(description="Unique monitor identifier")] organization_id: Annotated[ - int, + int | None, Field( alias="organizationId", description="Organization this monitor belongs to" ), - ] + ] = None name: Annotated[ str, Field(description="Human-readable name for this monitor", min_length=1) ] @@ -5990,13 +6016,15 @@ class MonitorDto(BaseModel): | TcpMonitorConfig ) frequency_seconds: Annotated[ - int, + int | None, Field( alias="frequencySeconds", description="Check frequency in seconds (30–86400)", ), - ] - enabled: Annotated[bool, Field(description="Whether the monitor is active")] + ] = None + enabled: Annotated[ + bool | None, Field(description="Whether the monitor is active") + ] = None regions: Annotated[ list[str], Field(description="Probe regions where checks are executed") ] @@ -6068,8 +6096,8 @@ class MonitorVersionDto(BaseModel): UUID, Field(alias="monitorId", description="Monitor this version belongs to") ] version: Annotated[ - int, Field(description="Monotonically increasing version number") - ] + int | None, Field(description="Monotonically increasing version number") + ] = None snapshot: MonitorDto changed_by_id: Annotated[ int | None, @@ -6100,11 +6128,11 @@ class MonitorVersionDto(BaseModel): class NotificationPolicyDto(BaseModel): id: Annotated[UUID, Field(description="Unique notification policy identifier")] organization_id: Annotated[ - int, + int | None, Field( alias="organizationId", description="Organization this policy belongs to" ), - ] + ] = None name: Annotated[ str, Field(description="Human-readable name for this policy", min_length=1) ] @@ -6116,10 +6144,13 @@ class NotificationPolicyDto(BaseModel): ), ] escalation: EscalationChain - enabled: Annotated[bool, Field(description="Whether this policy is active")] + enabled: Annotated[ + bool | None, Field(description="Whether this policy is active") + ] = None priority: Annotated[ - int, Field(description="Evaluation order; higher value = evaluated first") - ] + int | None, + Field(description="Evaluation order; higher value = evaluated first"), + ] = None created_at: Annotated[ AwareDatetime, Field(alias="createdAt", description="Timestamp when the policy was created"), @@ -6135,9 +6166,9 @@ class NotificationPolicyDto(BaseModel): class ResourceGroupDto(BaseModel): id: Annotated[UUID, Field(description="Unique resource group identifier")] organization_id: Annotated[ - int, + int | None, Field(alias="organizationId", description="Organization this group belongs to"), - ] + ] = None name: Annotated[str, Field(description="Human-readable group name", min_length=1)] slug: Annotated[str, Field(description="URL-safe group identifier", min_length=1)] description: Annotated[ @@ -6192,12 +6223,12 @@ class ResourceGroupDto(BaseModel): Field(alias="healthThresholdValue", description="Health threshold value"), ] = None suppress_member_alerts: Annotated[ - bool, + bool | None, Field( alias="suppressMemberAlerts", description="When true, member-level incidents skip notification dispatch; only group alerts fire", ), - ] + ] = None confirmation_delay_seconds: Annotated[ int | None, Field( @@ -6240,8 +6271,10 @@ class ServiceDetailDto(BaseModel): developer_context: Annotated[str | None, Field(alias="developerContext")] = None logo_url: Annotated[str | None, Field(alias="logoUrl")] = None adapter_type: Annotated[str, Field(alias="adapterType")] - polling_interval_seconds: Annotated[int, Field(alias="pollingIntervalSeconds")] - enabled: bool + polling_interval_seconds: Annotated[ + int | None, Field(alias="pollingIntervalSeconds") + ] = None + enabled: bool | None = None created_at: Annotated[AwareDatetime, Field(alias="createdAt")] updated_at: Annotated[AwareDatetime, Field(alias="updatedAt")] current_status: Annotated[ServiceStatusDto | None, Field(alias="currentStatus")] = ( @@ -6429,12 +6462,12 @@ class StatusPageIncidentDto(BaseModel): title: Annotated[str, Field(min_length=1)] status: Status15 impact: Impact - scheduled: bool + scheduled: bool | None = None scheduled_for: Annotated[AwareDatetime | None, Field(alias="scheduledFor")] = None scheduled_until: Annotated[AwareDatetime | None, Field(alias="scheduledUntil")] = ( None ) - auto_resolve: Annotated[bool, Field(alias="autoResolve")] + auto_resolve: Annotated[bool | None, Field(alias="autoResolve")] = None incident_id: Annotated[UUID | None, Field(alias="incidentId")] = None started_at: Annotated[AwareDatetime, Field(alias="startedAt")] published_at: Annotated[AwareDatetime | None, Field(alias="publishedAt")] = None @@ -6452,88 +6485,88 @@ class StatusPageIncidentDto(BaseModel): class TableValueResultComponentUptimeDayDto(BaseModel): data: list[ComponentUptimeDayDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultIntegrationDto(BaseModel): data: list[IntegrationDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultMonitorDto(BaseModel): data: list[MonitorDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultMonitorVersionDto(BaseModel): data: list[MonitorVersionDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultNotificationPolicyDto(BaseModel): data: list[NotificationPolicyDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultResourceGroupDto(BaseModel): data: list[ResourceGroupDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultStatusPageIncidentDto(BaseModel): data: list[StatusPageIncidentDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultTagDto(BaseModel): data: list[TagDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultWebhookDeliveryDto(BaseModel): data: list[WebhookDeliveryDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultWebhookEndpointDto(BaseModel): data: list[WebhookEndpointDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultWorkspaceDto(BaseModel): data: list[WorkspaceDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] + has_next: Annotated[bool | None, Field(alias="hasNext")] = None + has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None From e2e8eab2115b2fc6547865b1e79afd5b16f82578 Mon Sep 17 00:00:00 2001 From: caballeto Date: Sun, 19 Apr 2026 13:36:10 +0200 Subject: [PATCH 02/10] feat: add runtime request validation and block raw dict bypass Every resource method that accepts a body now validates it through the Pydantic model before sending. Raw dicts are rejected in _serialize_body to prevent bypassing validation. Dicts can still be used by passing them through validate_request() first. Made-with: Cursor --- src/devhelm/_generated.py | 585 +++++++++--------- src/devhelm/_http.py | 13 +- src/devhelm/_validation.py | 23 +- src/devhelm/resources/alert_channels.py | 4 +- src/devhelm/resources/api_keys.py | 3 +- src/devhelm/resources/deploy_lock.py | 3 +- src/devhelm/resources/environments.py | 4 +- src/devhelm/resources/incidents.py | 5 +- src/devhelm/resources/monitors.py | 4 +- .../resources/notification_policies.py | 12 +- src/devhelm/resources/resource_groups.py | 11 +- src/devhelm/resources/secrets.py | 4 +- src/devhelm/resources/status_pages.py | 34 +- src/devhelm/resources/tags.py | 4 +- src/devhelm/resources/webhooks.py | 4 +- tests/test_http.py | 9 +- 16 files changed, 393 insertions(+), 329 deletions(-) diff --git a/src/devhelm/_generated.py b/src/devhelm/_generated.py index 81d8417..26a6c34 100644 --- a/src/devhelm/_generated.py +++ b/src/devhelm/_generated.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: .openapi-preprocessed.json -# timestamp: 2026-04-19T10:56:52+00:00 +# timestamp: 2026-04-19T11:34:57+00:00 from __future__ import annotations from typing import Annotated, Any @@ -242,23 +242,22 @@ class AlertDeliveryDto(BaseModel): ), ] step_number: Annotated[ - int | None, + int, Field( alias="stepNumber", description="1-based escalation step this delivery belongs to", ), - ] = None + ] fire_count: Annotated[ - int | None, + int, Field( alias="fireCount", description="Fire sequence within the step: 1 = initial, 2+ = repeat re-fires", ), - ] = None + ] attempt_count: Annotated[ - int | None, - Field(alias="attemptCount", description="Number of delivery attempts made"), - ] = None + int, Field(alias="attemptCount", description="Number of delivery attempts made") + ] last_attempt_at: Annotated[ AwareDatetime | None, Field(alias="lastAttemptAt", description="When the last attempt was made"), @@ -288,7 +287,7 @@ class AlertDeliveryDto(BaseModel): class ApiKeyCreateResponse(BaseModel): - id: Annotated[int | None, Field(description="Unique API key identifier")] = None + id: Annotated[int, Field(description="Unique API key identifier")] name: Annotated[ str, Field(description="Human-readable name for this API key", min_length=1) ] @@ -313,7 +312,7 @@ class ApiKeyCreateResponse(BaseModel): class ApiKeyDto(BaseModel): - id: Annotated[int | None, Field(description="Unique API key identifier")] = None + id: Annotated[int, Field(description="Unique API key identifier")] name: Annotated[ str, Field(description="Human-readable name for this API key", min_length=1) ] @@ -362,9 +361,7 @@ class Severity(StrEnum): class AssertionResultDto(BaseModel): type: Annotated[str, Field(description="Assertion type", examples=["status_code"])] - passed: Annotated[ - bool | None, Field(description="Whether the assertion passed") - ] = None + passed: Annotated[bool, Field(description="Whether the assertion passed")] severity: Annotated[Severity, Field(description="Assertion severity")] message: Annotated[ str | None, Field(description="Human-readable result message") @@ -427,9 +424,7 @@ class AssertionTestResultDto(BaseModel): AssertionType, Field(alias="assertionType", description="Assertion type evaluated"), ] - passed: Annotated[ - bool | None, Field(description="Whether the assertion passed") - ] = None + passed: Annotated[bool, Field(description="Whether the assertion passed")] severity: Annotated[Severity, Field(description="Assertion severity: FAIL or WARN")] message: Annotated[str, Field(description="Human-readable result description")] expected: Annotated[str | None, Field(description="Expected value")] = None @@ -439,7 +434,7 @@ class AssertionTestResultDto(BaseModel): class AuditEventDto(BaseModel): - id: Annotated[int | None, Field(description="Unique audit event identifier")] = None + id: Annotated[int, Field(description="Unique audit event identifier")] actor_id: Annotated[ int | None, Field( @@ -507,9 +502,9 @@ class CategoryDto(BaseModel): str, Field(description="Category name (e.g. CI/CD, Cloud, Payments)") ] service_count: Annotated[ - int | None, + int, Field(alias="serviceCount", description="Number of services in this category"), - ] = None + ] class OrgRole(StrEnum): @@ -604,26 +599,26 @@ class ComponentImpact(BaseModel): ), ] = None uptime_percentage: Annotated[ - float | None, + float, Field( alias="uptimePercentage", description="Computed uptime % for this component on this day", ), - ] = None + ] partial_outage_seconds: Annotated[ - int | None, + int, Field( alias="partialOutageSeconds", description="Seconds of partial outage observed on this day", ), - ] = None + ] major_outage_seconds: Annotated[ - int | None, + int, Field( alias="majorOutageSeconds", description="Seconds of major outage observed on this day", ), - ] = None + ] class ComponentPosition(BaseModel): @@ -631,9 +626,8 @@ class ComponentPosition(BaseModel): UUID, Field(alias="componentId", description="Component ID") ] display_order: Annotated[ - int | None, - Field(alias="displayOrder", description="New display order (0-based)"), - ] = None + int, Field(alias="displayOrder", description="New display order (0-based)") + ] group_id: Annotated[ UUID | None, Field(alias="groupId", description="Target group ID, null for ungrouped"), @@ -683,19 +677,19 @@ class ConfirmationPolicy(BaseModel): Field(description="How incident confirmation is coordinated across regions"), ] min_regions_failing: Annotated[ - int | None, + int, Field( alias="minRegionsFailing", description="Minimum failing regions required to confirm an incident", ), - ] = None + ] max_wait_seconds: Annotated[ - int | None, + int, Field( alias="maxWaitSeconds", description="Maximum seconds to wait for enough regions to fail after first trigger", ), - ] = None + ] class CreateApiKeyRequest(BaseModel): @@ -737,12 +731,12 @@ class CreateEnvironmentRequest(BaseModel): Field(description="Initial key-value variable pairs for this environment"), ] = None is_default: Annotated[ - bool | None, + bool, Field( alias="isDefault", description="Whether this is the default environment for new monitors", ), - ] = None + ] class RoleOffered(StrEnum): @@ -1116,11 +1110,11 @@ class CursorPage(BaseModel): ), ] = None has_more: Annotated[ - bool | None, + bool, Field( alias="hasMore", description="Whether more results exist beyond this page" ), - ] = None + ] class DayIncident(BaseModel): @@ -1136,11 +1130,11 @@ class DayIncident(BaseModel): Impact, Field(description="Severity bucket (none, minor, major, critical)") ] scheduled: Annotated[ - bool | None, + bool, Field( description="True for scheduled maintenances; false for unplanned incidents" ), - ] = None + ] started_at: Annotated[ AwareDatetime | None, Field(alias="startedAt", description="Incident start timestamp"), @@ -1163,27 +1157,26 @@ class DayIncident(BaseModel): class DekRotationResultDto(BaseModel): previous_dek_version: Annotated[ - int | None, + int, Field(alias="previousDekVersion", description="DEK version before rotation"), - ] = None + ] new_dek_version: Annotated[ - int | None, - Field(alias="newDekVersion", description="DEK version after rotation"), - ] = None + int, Field(alias="newDekVersion", description="DEK version after rotation") + ] secrets_re_encrypted: Annotated[ - int | None, + int, Field( alias="secretsReEncrypted", description="Number of secrets re-encrypted with the new DEK", ), - ] = None + ] channels_re_encrypted: Annotated[ - int | None, + int, Field( alias="channelsReEncrypted", description="Number of alert channels re-encrypted with the new DEK", ), - ] = None + ] rotated_at: Annotated[ AwareDatetime, Field( @@ -1213,8 +1206,8 @@ class DeliveryAttemptDto(BaseModel): id: UUID delivery_id: Annotated[UUID, Field(alias="deliveryId")] attempt_number: Annotated[ - int | None, Field(alias="attemptNumber", description="1-based attempt number") - ] = None + int, Field(alias="attemptNumber", description="1-based attempt number") + ] status: Annotated[ str, Field(description="Outcome: SUCCESS, FAILED, TIMEOUT, ERROR") ] @@ -1459,26 +1452,25 @@ class EmailChannelConfig(ChannelConfig): class EntitlementDto(BaseModel): key: Annotated[str, Field(description="Entitlement key")] value: Annotated[ - int | None, Field(description="Effective limit value (overrides applied)") - ] = None + int, Field(description="Effective limit value (overrides applied)") + ] default_value: Annotated[ - int | None, + int, Field( alias="defaultValue", description="Plan-tier default value before overrides" ), - ] = None + ] overridden: Annotated[ - bool | None, - Field(description="Whether this entitlement has an org-level override"), - ] = None + bool, Field(description="Whether this entitlement has an org-level override") + ] class EnvironmentDto(BaseModel): id: Annotated[UUID, Field(description="Unique environment identifier")] org_id: Annotated[ - int | None, + int, Field(alias="orgId", description="Organization this environment belongs to"), - ] = None + ] name: Annotated[ str, Field(description="Human-readable environment name", min_length=1) ] @@ -1501,30 +1493,30 @@ class EnvironmentDto(BaseModel): ), ] monitor_count: Annotated[ - int | None, + int, Field( alias="monitorCount", description="Number of monitors using this environment", ), - ] = None + ] is_default: Annotated[ - bool | None, + bool, Field( alias="isDefault", description="Whether this is the default environment for new monitors", ), - ] = None + ] class EscalationStep(BaseModel): delay_minutes: Annotated[ - int | None, + int, Field( alias="delayMinutes", description="Minutes to wait before executing this step (0 = immediate)", ge=0, ), - ] = None + ] channel_ids: Annotated[ list[UUID], Field( @@ -1715,11 +1707,11 @@ class IncidentDto(BaseModel): ), ] = None organization_id: Annotated[ - int | None, + int, Field( alias="organizationId", description="Organization this incident belongs to" ), - ] = None + ] source: Annotated[ Source, Field(description="Incident origin: MONITOR, SERVICE, or MANUAL") ] @@ -1750,12 +1742,12 @@ class IncidentDto(BaseModel): ), ] reopen_count: Annotated[ - int | None, + int, Field( alias="reopenCount", description="Number of times this incident has been reopened", ), - ] = None + ] created_by_user_id: Annotated[ int | None, Field( @@ -1764,12 +1756,12 @@ class IncidentDto(BaseModel): ), ] = None status_page_visible: Annotated[ - bool | None, + bool, Field( alias="statusPageVisible", description="Whether this incident is visible on the status page", ), - ] = None + ] service_incident_id: Annotated[ UUID | None, Field( @@ -1939,7 +1931,7 @@ class IncidentUpdateDto(BaseModel): new_status: Annotated[NewStatus | None, Field(alias="newStatus")] = None body: str | None = None created_by: Annotated[CreatedBy | None, Field(alias="createdBy")] = None - notify_subscribers: Annotated[bool | None, Field(alias="notifySubscribers")] = None + notify_subscribers: Annotated[bool, Field(alias="notifySubscribers")] created_at: Annotated[AwareDatetime, Field(alias="createdAt")] @@ -1966,8 +1958,8 @@ class IntegrationFieldDto(BaseModel): class InviteDto(BaseModel): invite_id: Annotated[ - int | None, Field(alias="inviteId", description="Unique invite identifier") - ] = None + int, Field(alias="inviteId", description="Unique invite identifier") + ] email: Annotated[str, Field(description="Email address the invite was sent to")] role_offered: Annotated[ RoleOffered, @@ -2016,7 +2008,7 @@ class JsonPathAssertion(AssertionConfig): class KeyInfo(BaseModel): - id: Annotated[int | None, Field(description="Key ID")] = None + id: Annotated[int, Field(description="Key ID")] name: Annotated[str, Field(description="Human-readable key name")] created_at: Annotated[ AwareDatetime, Field(alias="createdAt", description="When the key was created") @@ -2046,7 +2038,7 @@ class LinkedStatusPageIncidentDto(BaseModel): title: str status: Status8 impact: Impact - scheduled: bool | None = None + scheduled: bool published_at: Annotated[AwareDatetime | None, Field(alias="publishedAt")] = None @@ -2080,12 +2072,12 @@ class MaintenanceWindowDto(BaseModel): ), ] = None organization_id: Annotated[ - int | None, + int, Field( alias="organizationId", description="Organization this maintenance window belongs to", ), - ] = None + ] starts_at: Annotated[ AwareDatetime, Field( @@ -2107,12 +2099,12 @@ class MaintenanceWindowDto(BaseModel): str | None, Field(description="Human-readable reason for the maintenance") ] = None suppress_alerts: Annotated[ - bool | None, + bool, Field( alias="suppressAlerts", description="Whether alerts are suppressed during this window", ), - ] = None + ] created_at: Annotated[ AwareDatetime, Field(alias="createdAt", description="Timestamp when the window was created"), @@ -2225,8 +2217,8 @@ class Status9(StrEnum): class MemberDto(BaseModel): user_id: Annotated[ - int | None, Field(alias="userId", description="User identifier of the member") - ] = None + int, Field(alias="userId", description="User identifier of the member") + ] email: Annotated[str, Field(description="Member email address")] name: Annotated[ str | None, Field(description="Member display name; null if not set") @@ -2313,7 +2305,7 @@ class MonitorsSummaryDto(BaseModel): class MonitorTestResultDto(BaseModel): - passed: bool | None = None + passed: bool error: str | None = None status_code: Annotated[int | None, Field(alias="statusCode")] = None response_time_ms: Annotated[int | None, Field(alias="responseTimeMs")] = None @@ -2391,12 +2383,12 @@ class NotificationDispatchDto(BaseModel): ), ] = None current_step: Annotated[ - int | None, + int, Field( alias="currentStep", description="1-based index of the currently active escalation step", ), - ] = None + ] total_steps: Annotated[ int | None, Field( @@ -2445,9 +2437,7 @@ class NotificationDispatchDto(BaseModel): class NotificationDto(BaseModel): - id: Annotated[int | None, Field(description="Unique notification identifier")] = ( - None - ) + id: Annotated[int, Field(description="Unique notification identifier")] type: Annotated[ str, Field(description="Notification category (e.g. incident, monitor, team)") ] @@ -2470,9 +2460,7 @@ class NotificationDto(BaseModel): description="ID of the resource this notification is about", ), ] = None - read: Annotated[ - bool | None, Field(description="Whether the notification has been read") - ] = None + read: Annotated[bool, Field(description="Whether the notification has been read")] created_at: Annotated[ AwareDatetime, Field( @@ -2496,9 +2484,7 @@ class OpsGenieChannelConfig(ChannelConfig): class OrganizationDto(BaseModel): - id: Annotated[int | None, Field(description="Unique organization identifier")] = ( - None - ) + id: Annotated[int, Field(description="Unique organization identifier")] name: Annotated[str, Field(description="Organization name")] email: Annotated[str, Field(description="Billing and contact email")] size: Annotated[ @@ -2513,7 +2499,7 @@ class OrganizationDto(BaseModel): class OrgInfo(BaseModel): - id: Annotated[int | None, Field(description="Organization ID")] = None + id: Annotated[int, Field(description="Organization ID")] name: Annotated[str, Field(description="Organization name")] @@ -2576,9 +2562,8 @@ class PlanInfo(BaseModel): ), ] = None trial_active: Annotated[ - bool | None, - Field(alias="trialActive", description="Whether the org is on a trial"), - ] = None + bool, Field(alias="trialActive", description="Whether the org is on a trial") + ] trial_expires_at: Annotated[ AwareDatetime | None, Field( @@ -2616,11 +2601,11 @@ class PollChartBucketDto(BaseModel): ), ] = None total_polls: Annotated[ - int | None, + int, Field( alias="totalPolls", description="Total polls in this bucket", examples=[60] ), - ] = None + ] class Status11(StrEnum): @@ -2741,11 +2726,11 @@ class RegexBodyAssertion(AssertionConfig): class RegionStatusDto(BaseModel): region: Annotated[str, Field(description="Region identifier", examples=["us-east"])] passed: Annotated[ - bool | None, + bool, Field( description="Whether the last check in this region passed", examples=[True] ), - ] = None + ] response_time_ms: Annotated[ int | None, Field( @@ -2826,23 +2811,23 @@ class ResourceGroupHealthDto(BaseModel): Status12, Field(description="Worst-of health status across all members") ] total_members: Annotated[ - int | None, + int, Field(alias="totalMembers", description="Total number of members in the group"), - ] = None + ] operational_count: Annotated[ - int | None, + int, Field( alias="operationalCount", description="Number of members currently in operational status", ), - ] = None + ] active_incidents: Annotated[ - int | None, + int, Field( alias="activeIncidents", description="Number of members with an active incident or non-operational status", ), - ] = None + ] threshold_status: Annotated[ ThresholdStatus | None, Field( @@ -3041,15 +3026,15 @@ class RetryStrategy(BaseModel): Field(description="Retry strategy kind, e.g. fixed interval between attempts"), ] max_retries: Annotated[ - int | None, + int, Field( alias="maxRetries", description="Maximum number of retries after a failed check", ), - ] = None + ] interval: Annotated[ - int | None, Field(description="Delay between retry attempts in seconds") - ] = None + int, Field(description="Delay between retry attempts in seconds") + ] class ScheduledMaintenanceDto(BaseModel): @@ -3119,11 +3104,11 @@ class SecretDto(BaseModel): str, Field(description="Secret key name, unique within the workspace") ] dek_version: Annotated[ - int | None, + int, Field( alias="dekVersion", description="DEK version at the time of last encryption" ), - ] = None + ] value_hash: Annotated[ str, Field( @@ -3176,18 +3161,14 @@ class ServiceCatalogDto(BaseModel): developer_context: Annotated[str | None, Field(alias="developerContext")] = None logo_url: Annotated[str | None, Field(alias="logoUrl")] = None adapter_type: Annotated[str, Field(alias="adapterType")] - polling_interval_seconds: Annotated[ - int | None, Field(alias="pollingIntervalSeconds") - ] = None - enabled: bool | None = None - published: bool | None = None + polling_interval_seconds: Annotated[int, Field(alias="pollingIntervalSeconds")] + enabled: bool + published: bool overall_status: Annotated[str | None, Field(alias="overallStatus")] = None created_at: Annotated[AwareDatetime, Field(alias="createdAt")] updated_at: Annotated[AwareDatetime, Field(alias="updatedAt")] - component_count: Annotated[int | None, Field(alias="componentCount")] = None - active_incident_count: Annotated[int | None, Field(alias="activeIncidentCount")] = ( - None - ) + component_count: Annotated[int, Field(alias="componentCount")] + active_incident_count: Annotated[int, Field(alias="activeIncidentCount")] data_completeness: Annotated[str, Field(alias="dataCompleteness")] uptime30d: Annotated[ float | None, @@ -3203,10 +3184,8 @@ class ServiceComponentDto(BaseModel): description: str | None = None group_id: Annotated[UUID | None, Field(alias="groupId")] = None position: int | None = None - showcase: bool | None = None - only_show_if_degraded: Annotated[bool | None, Field(alias="onlyShowIfDegraded")] = ( - None - ) + showcase: bool + only_show_if_degraded: Annotated[bool, Field(alias="onlyShowIfDegraded")] start_date: Annotated[AwareDatetime | None, Field(alias="startDate")] = None vendor_created_at: Annotated[ AwareDatetime | None, Field(alias="vendorCreatedAt") @@ -3221,12 +3200,12 @@ class ServiceComponentDto(BaseModel): ), ] has_uptime: Annotated[ - bool | None, + bool, Field( alias="hasUptime", description="Whether uptime data is available for this component", ), - ] = None + ] region: Annotated[ str | None, Field( @@ -3238,12 +3217,12 @@ class ServiceComponentDto(BaseModel): Field(alias="groupName", description="Display name of the parent group"), ] = None display_aggregated_uptime: Annotated[ - bool | None, + bool, Field( alias="displayAggregatedUptime", description="Group-only: render an aggregated uptime bar above this group's children", ), - ] = None + ] child_count: Annotated[ int | None, Field( @@ -3257,7 +3236,7 @@ class ServiceComponentDto(BaseModel): ] = None first_seen_at: Annotated[AwareDatetime, Field(alias="firstSeenAt")] last_seen_at: Annotated[AwareDatetime, Field(alias="lastSeenAt")] - is_group: Annotated[bool | None, Field(alias="isGroup")] = None + is_group: Annotated[bool, Field(alias="isGroup")] class ServiceDayDetailDto(BaseModel): @@ -3272,19 +3251,19 @@ class ServiceDayDetailDto(BaseModel): ), ] = None total_partial_outage_seconds: Annotated[ - int | None, + int, Field( alias="totalPartialOutageSeconds", description="Sum of partial outage seconds across all leaf components", ), - ] = None + ] total_major_outage_seconds: Annotated[ - int | None, + int, Field( alias="totalMajorOutageSeconds", description="Sum of major outage seconds across all leaf components", ), - ] = None + ] components: Annotated[ list[ComponentImpact], Field( @@ -3340,12 +3319,12 @@ class ServiceLiveStatusDto(BaseModel): ), ] active_incident_count: Annotated[ - int | None, + int, Field( alias="activeIncidentCount", description="Number of currently unresolved incidents", ), - ] = None + ] last_polled_at: Annotated[ str | None, Field( @@ -3386,8 +3365,8 @@ class ServicePollResultDto(BaseModel): ), ] = None passed: Annotated[ - bool | None, Field(description="Whether the poll succeeded", examples=[True]) - ] = None + bool, Field(description="Whether the poll succeeded", examples=[True]) + ] failure_reason: Annotated[ str | None, Field( @@ -3395,21 +3374,21 @@ class ServicePollResultDto(BaseModel): ), ] = None component_count: Annotated[ - int | None, + int, Field( alias="componentCount", description="Number of components reported by the service", examples=[12], ), - ] = None + ] degraded_count: Annotated[ - int | None, + int, Field( alias="degradedCount", description="Number of degraded or non-operational components", examples=[1], ), - ] = None + ] class ServicePollSummaryDto(BaseModel): @@ -3422,21 +3401,21 @@ class ServicePollSummaryDto(BaseModel): ), ] = None total_polls: Annotated[ - int | None, + int, Field( alias="totalPolls", description="Total number of polls executed", examples=[4320], ), - ] = None + ] passed_polls: Annotated[ - int | None, + int, Field( alias="passedPolls", description="Number of polls that succeeded", examples=[4318], ), - ] = None + ] avg_response_time_ms: Annotated[ float | None, Field( @@ -3506,10 +3485,8 @@ class ServiceSubscriptionDto(BaseModel): category: str | None = None official_status_url: Annotated[str | None, Field(alias="officialStatusUrl")] = None adapter_type: Annotated[str, Field(alias="adapterType", min_length=1)] - polling_interval_seconds: Annotated[ - int | None, Field(alias="pollingIntervalSeconds") - ] = None - enabled: bool | None = None + polling_interval_seconds: Annotated[int, Field(alias="pollingIntervalSeconds")] + enabled: bool logo_url: Annotated[ str | None, Field(alias="logoUrl", description="Logo URL from the service catalog"), @@ -3838,12 +3815,10 @@ class StatusPageComponentDto(BaseModel): monitor_id: Annotated[UUID | None, Field(alias="monitorId")] = None resource_group_id: Annotated[UUID | None, Field(alias="resourceGroupId")] = None current_status: Annotated[CurrentStatus1, Field(alias="currentStatus")] - show_uptime: Annotated[bool | None, Field(alias="showUptime")] = None - display_order: Annotated[int | None, Field(alias="displayOrder")] = None - page_order: Annotated[int | None, Field(alias="pageOrder")] = None - exclude_from_overall: Annotated[bool | None, Field(alias="excludeFromOverall")] = ( - None - ) + show_uptime: Annotated[bool, Field(alias="showUptime")] + display_order: Annotated[int, Field(alias="displayOrder")] + page_order: Annotated[int, Field(alias="pageOrder")] + exclude_from_overall: Annotated[bool, Field(alias="excludeFromOverall")] start_date: Annotated[AwareDatetime | None, Field(alias="startDate")] = None created_at: Annotated[AwareDatetime, Field(alias="createdAt")] updated_at: Annotated[AwareDatetime, Field(alias="updatedAt")] @@ -3854,9 +3829,9 @@ class StatusPageComponentGroupDto(BaseModel): status_page_id: Annotated[UUID, Field(alias="statusPageId")] name: str description: str | None = None - display_order: Annotated[int | None, Field(alias="displayOrder")] = None - page_order: Annotated[int | None, Field(alias="pageOrder")] = None - collapsed: bool | None = None + display_order: Annotated[int, Field(alias="displayOrder")] + page_order: Annotated[int, Field(alias="pageOrder")] + collapsed: bool components: list[StatusPageComponentDto] | None = None created_at: Annotated[AwareDatetime, Field(alias="createdAt")] updated_at: Annotated[AwareDatetime, Field(alias="updatedAt")] @@ -3890,7 +3865,7 @@ class StatusPageCustomDomainDto(BaseModel): verification_error: Annotated[str | None, Field(alias="verificationError")] = None created_at: Annotated[AwareDatetime, Field(alias="createdAt")] updated_at: Annotated[AwareDatetime, Field(alias="updatedAt")] - primary: bool | None = None + primary: bool class OverallStatus(StrEnum): @@ -3903,14 +3878,14 @@ class OverallStatus(StrEnum): class StatusPageDto(BaseModel): id: UUID - organization_id: Annotated[int | None, Field(alias="organizationId")] = None - workspace_id: Annotated[int | None, Field(alias="workspaceId")] = None + organization_id: Annotated[int, Field(alias="organizationId")] + workspace_id: Annotated[int, Field(alias="workspaceId")] name: Annotated[str, Field(min_length=1)] slug: Annotated[str, Field(min_length=1)] description: str | None = None branding: StatusPageBranding visibility: Visibility - enabled: bool | None = None + enabled: bool incident_mode: Annotated[IncidentMode, Field(alias="incidentMode")] component_count: Annotated[int | None, Field(alias="componentCount")] = None subscriber_count: Annotated[int | None, Field(alias="subscriberCount")] = None @@ -3951,14 +3926,14 @@ class StatusPageIncidentUpdateDto(BaseModel): body: str created_by: Annotated[CreatedBy1 | None, Field(alias="createdBy")] = None created_by_user_id: Annotated[int | None, Field(alias="createdByUserId")] = None - notify_subscribers: Annotated[bool | None, Field(alias="notifySubscribers")] = None + notify_subscribers: Annotated[bool, Field(alias="notifySubscribers")] created_at: Annotated[AwareDatetime, Field(alias="createdAt")] class StatusPageSubscriberDto(BaseModel): id: UUID email: str - confirmed: bool | None = None + confirmed: bool created_at: Annotated[AwareDatetime, Field(alias="createdAt")] @@ -3970,184 +3945,184 @@ class Summary(BaseModel): class TableValueResultAlertChannelDto(BaseModel): data: list[AlertChannelDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultAlertDeliveryDto(BaseModel): data: list[AlertDeliveryDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultApiKeyDto(BaseModel): data: list[ApiKeyDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultAuditEventDto(BaseModel): data: list[AuditEventDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultCategoryDto(BaseModel): data: list[CategoryDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultDeliveryAttemptDto(BaseModel): data: list[DeliveryAttemptDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultEnvironmentDto(BaseModel): data: list[EnvironmentDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultIncidentDto(BaseModel): data: list[IncidentDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultInviteDto(BaseModel): data: list[InviteDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultMaintenanceWindowDto(BaseModel): data: list[MaintenanceWindowDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultMemberDto(BaseModel): data: list[MemberDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultNotificationDispatchDto(BaseModel): data: list[NotificationDispatchDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultNotificationDto(BaseModel): data: list[NotificationDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultScheduledMaintenanceDto(BaseModel): data: list[ScheduledMaintenanceDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultSecretDto(BaseModel): data: list[SecretDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultServiceComponentDto(BaseModel): data: list[ServiceComponentDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultServiceIncidentDto(BaseModel): data: list[ServiceIncidentDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultServiceSubscriptionDto(BaseModel): data: list[ServiceSubscriptionDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultStatusPageComponentDto(BaseModel): data: list[StatusPageComponentDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultStatusPageComponentGroupDto(BaseModel): data: list[StatusPageComponentGroupDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultStatusPageCustomDomainDto(BaseModel): data: list[StatusPageCustomDomainDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultStatusPageDto(BaseModel): data: list[StatusPageDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultStatusPageSubscriberDto(BaseModel): data: list[StatusPageSubscriberDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None @@ -4155,9 +4130,9 @@ class TableValueResultStatusPageSubscriberDto(BaseModel): class TagDto(BaseModel): id: Annotated[UUID, Field(description="Unique tag identifier")] organization_id: Annotated[ - int | None, + int, Field(alias="organizationId", description="Organization this tag belongs to"), - ] = None + ] name: Annotated[ str, Field(description="Tag name, unique within the org", min_length=1) ] @@ -4222,17 +4197,17 @@ class TeamsChannelConfig(ChannelConfig): class TestChannelResult(BaseModel): - success: bool | None = None + success: bool message: str class TestMatchResult(BaseModel): matched: Annotated[ - bool | None, + bool, Field( description="Whether the policy would match the supplied incident context" ), - ] = None + ] matched_rules: Annotated[ list[str], Field(alias="matchedRules", description="Rules that passed evaluation"), @@ -4954,13 +4929,13 @@ class UptimeBucketDto(BaseModel): ), ] = None total_polls: Annotated[ - int | None, + int, Field( alias="totalPolls", description="Total number of polls recorded in this bucket", examples=[12], ), - ] = None + ] class UptimeDto(BaseModel): @@ -5036,8 +5011,8 @@ class WebhookDeliveryDto(BaseModel): event_id: Annotated[str, Field(alias="eventId")] event_type: Annotated[str, Field(alias="eventType")] status: str - attempt_count: Annotated[int | None, Field(alias="attemptCount")] = None - max_attempts: Annotated[int | None, Field(alias="maxAttempts")] = None + attempt_count: Annotated[int, Field(alias="attemptCount")] + max_attempts: Annotated[int, Field(alias="maxAttempts")] response_status: Annotated[int | None, Field(alias="responseStatus")] = None response_latency_ms: Annotated[int | None, Field(alias="responseLatencyMs")] = None error_message: Annotated[str | None, Field(alias="errorMessage")] = None @@ -5063,15 +5038,15 @@ class WebhookEndpointDto(BaseModel): ), ] enabled: Annotated[ - bool | None, Field(description="Whether delivery is enabled for this endpoint") - ] = None + bool, Field(description="Whether delivery is enabled for this endpoint") + ] consecutive_failures: Annotated[ - int | None, + int, Field( alias="consecutiveFailures", description="Number of consecutive delivery failures", ), - ] = None + ] disabled_reason: Annotated[ str | None, Field( @@ -5127,14 +5102,14 @@ class WebhookSigningSecretDto(BaseModel): class WebhookTestResult(BaseModel): - success: bool | None = None + success: bool status_code: Annotated[int | None, Field(alias="statusCode")] = None message: str duration_ms: Annotated[int | None, Field(alias="durationMs")] = None class WorkspaceDto(BaseModel): - id: Annotated[int | None, Field(description="Unique workspace identifier")] = None + id: Annotated[int, Field(description="Unique workspace identifier")] created_at: Annotated[ AwareDatetime, Field( @@ -5150,9 +5125,8 @@ class WorkspaceDto(BaseModel): ] name: Annotated[str, Field(description="Workspace name", min_length=1)] org_id: Annotated[ - int | None, - Field(alias="orgId", description="Organization this workspace belongs to"), - ] = None + int, Field(alias="orgId", description="Organization this workspace belongs to") + ] class AddMonitorTagsRequest(BaseModel): @@ -5330,8 +5304,8 @@ class CheckResultDto(BaseModel): ), ] = None passed: Annotated[ - bool | None, Field(description="Whether the check passed", examples=[True]) - ] = None + bool, Field(description="Whether the check passed", examples=[True]) + ] failure_reason: Annotated[ str | None, Field( @@ -5361,26 +5335,26 @@ class ComponentUptimeDayDto(BaseModel): Field(description="Start-of-day timestamp for this bucket (UTC midnight)"), ] partial_outage_seconds: Annotated[ - int | None, + int, Field( alias="partialOutageSeconds", description="Seconds of partial outage on this day", ), - ] = None + ] major_outage_seconds: Annotated[ - int | None, + int, Field( alias="majorOutageSeconds", description="Seconds of major outage on this day", ), - ] = None + ] uptime_percentage: Annotated[ - float | None, + float, Field( alias="uptimePercentage", description="Computed uptime percentage using weighted formula", ), - ] = None + ] incidents: Annotated[ list[IncidentRef] | None, Field(description="Incidents that overlapped this day"), @@ -5608,11 +5582,11 @@ class CursorPageCheckResultDto(BaseModel): ), ] = None has_more: Annotated[ - bool | None, + bool, Field( alias="hasMore", description="Whether more results exist beyond this page" ), - ] = None + ] class CursorPageServiceCatalogDto(BaseModel): @@ -5625,11 +5599,11 @@ class CursorPageServiceCatalogDto(BaseModel): ), ] = None has_more: Annotated[ - bool | None, + bool, Field( alias="hasMore", description="Whether more results exist beyond this page" ), - ] = None + ] class CursorPageServicePollResultDto(BaseModel): @@ -5642,11 +5616,11 @@ class CursorPageServicePollResultDto(BaseModel): ), ] = None has_more: Annotated[ - bool | None, + bool, Field( alias="hasMore", description="Whether more results exist beyond this page" ), - ] = None + ] class DashboardOverviewDto(BaseModel): @@ -5701,58 +5675,58 @@ class EscalationChain(BaseModel): class GlobalStatusSummaryDto(BaseModel): total_services: Annotated[ - int | None, + int, Field( alias="totalServices", description="Total number of services in the catalog" ), - ] = None + ] operational_count: Annotated[ - int | None, + int, Field( alias="operationalCount", description="Number of services currently fully operational", ), - ] = None + ] degraded_count: Annotated[ - int | None, + int, Field( alias="degradedCount", description="Number of services with degraded status" ), - ] = None + ] partial_outage_count: Annotated[ - int | None, + int, Field( alias="partialOutageCount", description="Number of services with partial outage", ), - ] = None + ] major_outage_count: Annotated[ - int | None, + int, Field( alias="majorOutageCount", description="Number of services with major outage" ), - ] = None + ] maintenance_count: Annotated[ - int | None, + int, Field( alias="maintenanceCount", description="Number of services currently under maintenance", ), - ] = None + ] unknown_count: Annotated[ - int | None, + int, Field( alias="unknownCount", description="Number of services with unknown or null status", ), - ] = None + ] active_incident_count: Annotated[ - int | None, + int, Field( alias="activeIncidentCount", description="Total number of active incidents across all services", ), - ] = None + ] services_with_issues: Annotated[ list[ServiceCatalogDto], Field( @@ -5998,11 +5972,11 @@ class MonitorAuthDto(BaseModel): class MonitorDto(BaseModel): id: Annotated[UUID, Field(description="Unique monitor identifier")] organization_id: Annotated[ - int | None, + int, Field( alias="organizationId", description="Organization this monitor belongs to" ), - ] = None + ] name: Annotated[ str, Field(description="Human-readable name for this monitor", min_length=1) ] @@ -6016,15 +5990,13 @@ class MonitorDto(BaseModel): | TcpMonitorConfig ) frequency_seconds: Annotated[ - int | None, + int, Field( alias="frequencySeconds", description="Check frequency in seconds (30–86400)", ), - ] = None - enabled: Annotated[ - bool | None, Field(description="Whether the monitor is active") - ] = None + ] + enabled: Annotated[bool, Field(description="Whether the monitor is active")] regions: Annotated[ list[str], Field(description="Probe regions where checks are executed") ] @@ -6096,8 +6068,8 @@ class MonitorVersionDto(BaseModel): UUID, Field(alias="monitorId", description="Monitor this version belongs to") ] version: Annotated[ - int | None, Field(description="Monotonically increasing version number") - ] = None + int, Field(description="Monotonically increasing version number") + ] snapshot: MonitorDto changed_by_id: Annotated[ int | None, @@ -6128,11 +6100,11 @@ class MonitorVersionDto(BaseModel): class NotificationPolicyDto(BaseModel): id: Annotated[UUID, Field(description="Unique notification policy identifier")] organization_id: Annotated[ - int | None, + int, Field( alias="organizationId", description="Organization this policy belongs to" ), - ] = None + ] name: Annotated[ str, Field(description="Human-readable name for this policy", min_length=1) ] @@ -6144,13 +6116,10 @@ class NotificationPolicyDto(BaseModel): ), ] escalation: EscalationChain - enabled: Annotated[ - bool | None, Field(description="Whether this policy is active") - ] = None + enabled: Annotated[bool, Field(description="Whether this policy is active")] priority: Annotated[ - int | None, - Field(description="Evaluation order; higher value = evaluated first"), - ] = None + int, Field(description="Evaluation order; higher value = evaluated first") + ] created_at: Annotated[ AwareDatetime, Field(alias="createdAt", description="Timestamp when the policy was created"), @@ -6166,9 +6135,9 @@ class NotificationPolicyDto(BaseModel): class ResourceGroupDto(BaseModel): id: Annotated[UUID, Field(description="Unique resource group identifier")] organization_id: Annotated[ - int | None, + int, Field(alias="organizationId", description="Organization this group belongs to"), - ] = None + ] name: Annotated[str, Field(description="Human-readable group name", min_length=1)] slug: Annotated[str, Field(description="URL-safe group identifier", min_length=1)] description: Annotated[ @@ -6223,12 +6192,12 @@ class ResourceGroupDto(BaseModel): Field(alias="healthThresholdValue", description="Health threshold value"), ] = None suppress_member_alerts: Annotated[ - bool | None, + bool, Field( alias="suppressMemberAlerts", description="When true, member-level incidents skip notification dispatch; only group alerts fire", ), - ] = None + ] confirmation_delay_seconds: Annotated[ int | None, Field( @@ -6271,10 +6240,8 @@ class ServiceDetailDto(BaseModel): developer_context: Annotated[str | None, Field(alias="developerContext")] = None logo_url: Annotated[str | None, Field(alias="logoUrl")] = None adapter_type: Annotated[str, Field(alias="adapterType")] - polling_interval_seconds: Annotated[ - int | None, Field(alias="pollingIntervalSeconds") - ] = None - enabled: bool | None = None + polling_interval_seconds: Annotated[int, Field(alias="pollingIntervalSeconds")] + enabled: bool created_at: Annotated[AwareDatetime, Field(alias="createdAt")] updated_at: Annotated[AwareDatetime, Field(alias="updatedAt")] current_status: Annotated[ServiceStatusDto | None, Field(alias="currentStatus")] = ( @@ -6462,12 +6429,12 @@ class StatusPageIncidentDto(BaseModel): title: Annotated[str, Field(min_length=1)] status: Status15 impact: Impact - scheduled: bool | None = None + scheduled: bool scheduled_for: Annotated[AwareDatetime | None, Field(alias="scheduledFor")] = None scheduled_until: Annotated[AwareDatetime | None, Field(alias="scheduledUntil")] = ( None ) - auto_resolve: Annotated[bool | None, Field(alias="autoResolve")] = None + auto_resolve: Annotated[bool, Field(alias="autoResolve")] incident_id: Annotated[UUID | None, Field(alias="incidentId")] = None started_at: Annotated[AwareDatetime, Field(alias="startedAt")] published_at: Annotated[AwareDatetime | None, Field(alias="publishedAt")] = None @@ -6485,88 +6452,88 @@ class StatusPageIncidentDto(BaseModel): class TableValueResultComponentUptimeDayDto(BaseModel): data: list[ComponentUptimeDayDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultIntegrationDto(BaseModel): data: list[IntegrationDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultMonitorDto(BaseModel): data: list[MonitorDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultMonitorVersionDto(BaseModel): data: list[MonitorVersionDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultNotificationPolicyDto(BaseModel): data: list[NotificationPolicyDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultResourceGroupDto(BaseModel): data: list[ResourceGroupDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultStatusPageIncidentDto(BaseModel): data: list[StatusPageIncidentDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultTagDto(BaseModel): data: list[TagDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultWebhookDeliveryDto(BaseModel): data: list[WebhookDeliveryDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultWebhookEndpointDto(BaseModel): data: list[WebhookEndpointDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None class TableValueResultWorkspaceDto(BaseModel): data: list[WorkspaceDto] - has_next: Annotated[bool | None, Field(alias="hasNext")] = None - has_prev: Annotated[bool | None, Field(alias="hasPrev")] = None + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] total_elements: Annotated[int | None, Field(alias="totalElements")] = None total_pages: Annotated[int | None, Field(alias="totalPages")] = None diff --git a/src/devhelm/_http.py b/src/devhelm/_http.py index def1416..992ecfa 100644 --- a/src/devhelm/_http.py +++ b/src/devhelm/_http.py @@ -65,9 +65,20 @@ def path_param(value: str | int) -> str: def _serialize_body(body: Any) -> Any: - """Convert a Pydantic model or dict to a JSON-serializable dict.""" + """Convert a Pydantic model to a JSON-serializable dict. + + Rejects raw dicts to prevent callers from bypassing Pydantic + validation. All request bodies must be validated model instances. + """ if isinstance(body, BaseModel): return body.model_dump(mode="json", by_alias=True, exclude_none=True) + if isinstance(body, dict): + raise DevhelmError( + "VALIDATION", + "Raw dicts are not accepted as request bodies. " + "Use the generated Pydantic model instead.", + 0, + ) return body diff --git a/src/devhelm/_validation.py b/src/devhelm/_validation.py index 171c062..7ca2e97 100644 --- a/src/devhelm/_validation.py +++ b/src/devhelm/_validation.py @@ -1,4 +1,4 @@ -"""Pydantic-based response validation for API responses. +"""Pydantic-based validation for API requests and responses. Parses raw JSON dicts through generated Pydantic models, catching shape mismatches before they propagate as silent bugs. @@ -15,6 +15,27 @@ M = TypeVar("M", bound=BaseModel) +def validate_request(model_class: type[M], body: Any, context: str = "") -> M: + """Validate a request body against its Pydantic model before sending. + + If *body* is already an instance of *model_class* it passes through. + Dicts and other mappings are coerced through ``model_validate``, + raising ``DevhelmError`` on constraint violations. + """ + if isinstance(body, model_class): + return body + try: + return model_class.model_validate(body) + except ValidationError as e: + ctx = f" ({context})" if context else "" + raise DevhelmError( + "VALIDATION", + f"Request validation failed{ctx}: {e.error_count()} error(s)", + 0, + str(e), + ) from e + + def parse_model(model_class: type[M], data: Any, context: str = "") -> M: """Parse a raw dict/JSON through a Pydantic model, raising DevhelmError on failure.""" try: diff --git a/src/devhelm/resources/alert_channels.py b/src/devhelm/resources/alert_channels.py index e2c37b6..ca9f592 100644 --- a/src/devhelm/resources/alert_channels.py +++ b/src/devhelm/resources/alert_channels.py @@ -10,7 +10,7 @@ ) from devhelm._http import api_delete, api_get, api_post, api_put, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single +from devhelm._validation import parse_single, validate_request class AlertChannels: @@ -39,6 +39,7 @@ def get(self, id: int | str) -> AlertChannelDto: def create(self, body: CreateAlertChannelRequest) -> AlertChannelDto: """Create a new alert channel.""" + body = validate_request(CreateAlertChannelRequest, body, "alertChannels.create") return parse_single( AlertChannelDto, api_post(self._client, "/api/v1/alert-channels", body), @@ -47,6 +48,7 @@ def create(self, body: CreateAlertChannelRequest) -> AlertChannelDto: def update(self, id: int | str, body: UpdateAlertChannelRequest) -> AlertChannelDto: """Update an alert channel.""" + body = validate_request(UpdateAlertChannelRequest, body, "alertChannels.update") return parse_single( AlertChannelDto, api_put(self._client, f"/api/v1/alert-channels/{path_param(id)}", body), diff --git a/src/devhelm/resources/api_keys.py b/src/devhelm/resources/api_keys.py index 8ef1fb2..c485aeb 100644 --- a/src/devhelm/resources/api_keys.py +++ b/src/devhelm/resources/api_keys.py @@ -5,7 +5,7 @@ from devhelm._generated import ApiKeyCreateResponse, ApiKeyDto, CreateApiKeyRequest from devhelm._http import api_delete, api_post, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single +from devhelm._validation import parse_single, validate_request class ApiKeys: @@ -24,6 +24,7 @@ def list_page(self, page: int, size: int) -> Page[ApiKeyDto]: def create(self, body: CreateApiKeyRequest) -> ApiKeyCreateResponse: """Create an API key. Returns the key value (shown only once).""" + body = validate_request(CreateApiKeyRequest, body, "apiKeys.create") return parse_single( ApiKeyCreateResponse, api_post(self._client, "/api/v1/api-keys", body), diff --git a/src/devhelm/resources/deploy_lock.py b/src/devhelm/resources/deploy_lock.py index 880f889..98d4ea7 100644 --- a/src/devhelm/resources/deploy_lock.py +++ b/src/devhelm/resources/deploy_lock.py @@ -4,7 +4,7 @@ from devhelm._generated import AcquireDeployLockRequest, DeployLockDto from devhelm._http import api_delete, api_get, api_post, path_param -from devhelm._validation import parse_model, parse_single +from devhelm._validation import parse_model, parse_single, validate_request class DeployLock: @@ -15,6 +15,7 @@ def __init__(self, client: httpx.Client) -> None: def acquire(self, body: AcquireDeployLockRequest) -> DeployLockDto: """Acquire a deploy lock.""" + body = validate_request(AcquireDeployLockRequest, body, "deployLock.acquire") return parse_single( DeployLockDto, api_post(self._client, "/api/v1/deploy/lock", body), diff --git a/src/devhelm/resources/environments.py b/src/devhelm/resources/environments.py index 086ddf0..b12d38c 100644 --- a/src/devhelm/resources/environments.py +++ b/src/devhelm/resources/environments.py @@ -9,7 +9,7 @@ ) from devhelm._http import api_delete, api_get, api_post, api_put, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single +from devhelm._validation import parse_single, validate_request class Environments: @@ -38,6 +38,7 @@ def get(self, slug: str) -> EnvironmentDto: def create(self, body: CreateEnvironmentRequest) -> EnvironmentDto: """Create an environment.""" + body = validate_request(CreateEnvironmentRequest, body, "environments.create") return parse_single( EnvironmentDto, api_post(self._client, "/api/v1/environments", body), @@ -46,6 +47,7 @@ def create(self, body: CreateEnvironmentRequest) -> EnvironmentDto: def update(self, slug: str, body: UpdateEnvironmentRequest) -> EnvironmentDto: """Update an environment.""" + body = validate_request(UpdateEnvironmentRequest, body, "environments.update") return parse_single( EnvironmentDto, api_put(self._client, f"/api/v1/environments/{path_param(slug)}", body), diff --git a/src/devhelm/resources/incidents.py b/src/devhelm/resources/incidents.py index 1088128..b0c234e 100644 --- a/src/devhelm/resources/incidents.py +++ b/src/devhelm/resources/incidents.py @@ -10,7 +10,7 @@ ) from devhelm._http import api_delete, api_get, api_post, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single +from devhelm._validation import parse_single, validate_request class Incidents: @@ -37,6 +37,7 @@ def get(self, id: int | str) -> IncidentDetailDto: def create(self, body: CreateManualIncidentRequest) -> IncidentDetailDto: """Create a manual incident.""" + body = validate_request(CreateManualIncidentRequest, body, "incidents.create") return parse_single( IncidentDetailDto, api_post(self._client, "/api/v1/incidents", body), @@ -47,6 +48,8 @@ def resolve( self, id: int | str, body: ResolveIncidentRequest | None = None ) -> IncidentDetailDto: """Resolve an incident.""" + if body is not None: + body = validate_request(ResolveIncidentRequest, body, "incidents.resolve") return parse_single( IncidentDetailDto, api_post(self._client, f"/api/v1/incidents/{path_param(id)}/resolve", body), diff --git a/src/devhelm/resources/monitors.py b/src/devhelm/resources/monitors.py index 348e3e8..064b0cf 100644 --- a/src/devhelm/resources/monitors.py +++ b/src/devhelm/resources/monitors.py @@ -18,7 +18,7 @@ fetch_cursor_page, fetch_page, ) -from devhelm._validation import parse_single +from devhelm._validation import parse_single, validate_request class Monitors: @@ -45,6 +45,7 @@ def get(self, id: int | str) -> MonitorDto: def create(self, body: CreateMonitorRequest) -> MonitorDto: """Create a new monitor.""" + body = validate_request(CreateMonitorRequest, body, "monitors.create") return parse_single( MonitorDto, api_post(self._client, "/api/v1/monitors", body), @@ -53,6 +54,7 @@ def create(self, body: CreateMonitorRequest) -> MonitorDto: def update(self, id: int | str, body: UpdateMonitorRequest) -> MonitorDto: """Update an existing monitor.""" + body = validate_request(UpdateMonitorRequest, body, "monitors.update") return parse_single( MonitorDto, api_put(self._client, f"/api/v1/monitors/{path_param(id)}", body), diff --git a/src/devhelm/resources/notification_policies.py b/src/devhelm/resources/notification_policies.py index 1d8c452..a67a1de 100644 --- a/src/devhelm/resources/notification_policies.py +++ b/src/devhelm/resources/notification_policies.py @@ -9,7 +9,7 @@ ) from devhelm._http import api_delete, api_get, api_post, api_put, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single +from devhelm._validation import parse_single, validate_request class NotificationPolicies: @@ -44,6 +44,9 @@ def get(self, id: int | str) -> NotificationPolicyDto: def create(self, body: CreateNotificationPolicyRequest) -> NotificationPolicyDto: """Create a notification policy.""" + body = validate_request( + CreateNotificationPolicyRequest, body, "notificationPolicies.create" + ) return parse_single( NotificationPolicyDto, api_post(self._client, "/api/v1/notification-policies", body), @@ -54,6 +57,9 @@ def update( self, id: int | str, body: UpdateNotificationPolicyRequest ) -> NotificationPolicyDto: """Update a notification policy.""" + body = validate_request( + UpdateNotificationPolicyRequest, body, "notificationPolicies.update" + ) return parse_single( NotificationPolicyDto, api_put( @@ -68,6 +74,4 @@ def delete(self, id: int | str) -> None: def test(self, id: int | str) -> None: """Send a test dispatch to verify policy routing.""" - api_post( - self._client, f"/api/v1/notification-policies/{path_param(id)}/test", {} - ) + api_post(self._client, f"/api/v1/notification-policies/{path_param(id)}/test") diff --git a/src/devhelm/resources/resource_groups.py b/src/devhelm/resources/resource_groups.py index f957bd4..ef55e52 100644 --- a/src/devhelm/resources/resource_groups.py +++ b/src/devhelm/resources/resource_groups.py @@ -11,7 +11,7 @@ ) from devhelm._http import api_delete, api_get, api_post, api_put, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single +from devhelm._validation import parse_single, validate_request class ResourceGroups: @@ -42,6 +42,9 @@ def get(self, id: int | str) -> ResourceGroupDto: def create(self, body: CreateResourceGroupRequest) -> ResourceGroupDto: """Create a resource group.""" + body = validate_request( + CreateResourceGroupRequest, body, "resourceGroups.create" + ) return parse_single( ResourceGroupDto, api_post(self._client, "/api/v1/resource-groups", body), @@ -52,6 +55,9 @@ def update( self, id: int | str, body: UpdateResourceGroupRequest ) -> ResourceGroupDto: """Update a resource group.""" + body = validate_request( + UpdateResourceGroupRequest, body, "resourceGroups.update" + ) return parse_single( ResourceGroupDto, api_put(self._client, f"/api/v1/resource-groups/{path_param(id)}", body), @@ -66,6 +72,9 @@ def add_member( self, group_id: int | str, body: AddResourceGroupMemberRequest ) -> ResourceGroupMemberDto: """Add a member to a resource group.""" + body = validate_request( + AddResourceGroupMemberRequest, body, "resourceGroups.addMember" + ) return parse_single( ResourceGroupMemberDto, api_post( diff --git a/src/devhelm/resources/secrets.py b/src/devhelm/resources/secrets.py index 1b9f459..adddaf9 100644 --- a/src/devhelm/resources/secrets.py +++ b/src/devhelm/resources/secrets.py @@ -5,7 +5,7 @@ from devhelm._generated import CreateSecretRequest, SecretDto, UpdateSecretRequest from devhelm._http import api_delete, api_post, api_put, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single +from devhelm._validation import parse_single, validate_request class Secrets: @@ -24,6 +24,7 @@ def list_page(self, page: int, size: int) -> Page[SecretDto]: def create(self, body: CreateSecretRequest) -> SecretDto: """Create a secret.""" + body = validate_request(CreateSecretRequest, body, "secrets.create") return parse_single( SecretDto, api_post(self._client, "/api/v1/secrets", body), @@ -32,6 +33,7 @@ def create(self, body: CreateSecretRequest) -> SecretDto: def update(self, key: str, body: UpdateSecretRequest) -> SecretDto: """Update a secret by key.""" + body = validate_request(UpdateSecretRequest, body, "secrets.update") return parse_single( SecretDto, api_put(self._client, f"/api/v1/secrets/{path_param(key)}", body), diff --git a/src/devhelm/resources/status_pages.py b/src/devhelm/resources/status_pages.py index 0ddd131..5b7f243 100644 --- a/src/devhelm/resources/status_pages.py +++ b/src/devhelm/resources/status_pages.py @@ -24,7 +24,7 @@ ) from devhelm._http import api_delete, api_get, api_post, api_put, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single +from devhelm._validation import parse_single, validate_request _BASE = "/api/v1/status-pages" @@ -49,6 +49,9 @@ def create( self, page_id: int | str, body: CreateStatusPageComponentRequest ) -> StatusPageComponentDto: """Add a component to a status page.""" + body = validate_request( + CreateStatusPageComponentRequest, body, "statusPages.components.create" + ) return parse_single( StatusPageComponentDto, api_post(self._client, f"{_page_path(page_id)}/components", body), @@ -62,6 +65,9 @@ def update( body: UpdateStatusPageComponentRequest, ) -> StatusPageComponentDto: """Update a component.""" + body = validate_request( + UpdateStatusPageComponentRequest, body, "statusPages.components.update" + ) return parse_single( StatusPageComponentDto, api_put( @@ -80,6 +86,9 @@ def delete(self, page_id: int | str, component_id: int | str) -> None: def reorder(self, page_id: int | str, body: ReorderComponentsRequest) -> None: """Batch reorder components.""" + body = validate_request( + ReorderComponentsRequest, body, "statusPages.components.reorder" + ) api_put(self._client, f"{_page_path(page_id)}/components/reorder", body) @@ -99,6 +108,9 @@ def create( self, page_id: int | str, body: CreateStatusPageComponentGroupRequest ) -> StatusPageComponentGroupDto: """Create a component group.""" + body = validate_request( + CreateStatusPageComponentGroupRequest, body, "statusPages.groups.create" + ) return parse_single( StatusPageComponentGroupDto, api_post(self._client, f"{_page_path(page_id)}/groups", body), @@ -112,6 +124,9 @@ def update( body: UpdateStatusPageComponentGroupRequest, ) -> StatusPageComponentGroupDto: """Update a component group.""" + body = validate_request( + UpdateStatusPageComponentGroupRequest, body, "statusPages.groups.update" + ) return parse_single( StatusPageComponentGroupDto, api_put( @@ -160,6 +175,9 @@ def create( self, page_id: int | str, body: CreateStatusPageIncidentRequest ) -> StatusPageIncidentDto: """Create a status page incident.""" + body = validate_request( + CreateStatusPageIncidentRequest, body, "statusPages.incidents.create" + ) return parse_single( StatusPageIncidentDto, api_post(self._client, f"{_page_path(page_id)}/incidents", body), @@ -173,6 +191,9 @@ def update( body: UpdateStatusPageIncidentRequest, ) -> StatusPageIncidentDto: """Update an incident.""" + body = validate_request( + UpdateStatusPageIncidentRequest, body, "statusPages.incidents.update" + ) return parse_single( StatusPageIncidentDto, api_put( @@ -190,6 +211,11 @@ def post_update( body: CreateStatusPageIncidentUpdateRequest, ) -> StatusPageIncidentDto: """Post a timeline update on an incident.""" + body = validate_request( + CreateStatusPageIncidentUpdateRequest, + body, + "statusPages.incidents.postUpdate", + ) return parse_single( StatusPageIncidentDto, api_post( @@ -249,6 +275,9 @@ def add( self, page_id: int | str, body: AdminAddSubscriberRequest ) -> StatusPageSubscriberDto: """Add a subscriber (admin).""" + body = validate_request( + AdminAddSubscriberRequest, body, "statusPages.subscribers.add" + ) return parse_single( StatusPageSubscriberDto, api_post(self._client, f"{_page_path(page_id)}/subscribers", body), @@ -279,6 +308,7 @@ def add( self, page_id: int | str, body: AddCustomDomainRequest ) -> StatusPageCustomDomainDto: """Add a custom domain.""" + body = validate_request(AddCustomDomainRequest, body, "statusPages.domains.add") return parse_single( StatusPageCustomDomainDto, api_post(self._client, f"{_page_path(page_id)}/domains", body), @@ -337,6 +367,7 @@ def get(self, id: int | str) -> StatusPageDto: def create(self, body: CreateStatusPageRequest) -> StatusPageDto: """Create a status page.""" + body = validate_request(CreateStatusPageRequest, body, "statusPages.create") return parse_single( StatusPageDto, api_post(self._client, _BASE, body), @@ -345,6 +376,7 @@ def create(self, body: CreateStatusPageRequest) -> StatusPageDto: def update(self, id: int | str, body: UpdateStatusPageRequest) -> StatusPageDto: """Update a status page.""" + body = validate_request(UpdateStatusPageRequest, body, "statusPages.update") return parse_single( StatusPageDto, api_put(self._client, _page_path(id), body), diff --git a/src/devhelm/resources/tags.py b/src/devhelm/resources/tags.py index efce14a..b2de157 100644 --- a/src/devhelm/resources/tags.py +++ b/src/devhelm/resources/tags.py @@ -5,7 +5,7 @@ from devhelm._generated import CreateTagRequest, TagDto, UpdateTagRequest from devhelm._http import api_delete, api_get, api_post, api_put, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single +from devhelm._validation import parse_single, validate_request class Tags: @@ -32,12 +32,14 @@ def get(self, id: int | str) -> TagDto: def create(self, body: CreateTagRequest) -> TagDto: """Create a tag.""" + body = validate_request(CreateTagRequest, body, "tags.create") return parse_single( TagDto, api_post(self._client, "/api/v1/tags", body), "POST /api/v1/tags" ) def update(self, id: int | str, body: UpdateTagRequest) -> TagDto: """Update a tag.""" + body = validate_request(UpdateTagRequest, body, "tags.update") return parse_single( TagDto, api_put(self._client, f"/api/v1/tags/{path_param(id)}", body), diff --git a/src/devhelm/resources/webhooks.py b/src/devhelm/resources/webhooks.py index e20f797..171d118 100644 --- a/src/devhelm/resources/webhooks.py +++ b/src/devhelm/resources/webhooks.py @@ -10,7 +10,7 @@ ) from devhelm._http import api_delete, api_get, api_post, api_put, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single +from devhelm._validation import parse_single, validate_request class Webhooks: @@ -39,6 +39,7 @@ def get(self, id: int | str) -> WebhookEndpointDto: def create(self, body: CreateWebhookEndpointRequest) -> WebhookEndpointDto: """Create a webhook endpoint.""" + body = validate_request(CreateWebhookEndpointRequest, body, "webhooks.create") return parse_single( WebhookEndpointDto, api_post(self._client, "/api/v1/webhooks", body), @@ -49,6 +50,7 @@ def update( self, id: int | str, body: UpdateWebhookEndpointRequest ) -> WebhookEndpointDto: """Update a webhook endpoint.""" + body = validate_request(UpdateWebhookEndpointRequest, body, "webhooks.update") return parse_single( WebhookEndpointDto, api_put(self._client, f"/api/v1/webhooks/{path_param(id)}", body), diff --git a/tests/test_http.py b/tests/test_http.py index b2d607a..344377a 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -178,11 +178,14 @@ def test_pydantic_model_serialized(self) -> None: result = _serialize_body(item) assert result == {"myField": "test"} - def test_dict_passthrough(self) -> None: + def test_dict_rejected(self) -> None: + import pytest + + from devhelm._errors import DevhelmError from devhelm._http import _serialize_body - d = {"key": "value"} - assert _serialize_body(d) is d + with pytest.raises(DevhelmError, match="Raw dicts are not accepted"): + _serialize_body({"key": "value"}) def test_none_passthrough(self) -> None: from devhelm._http import _serialize_body From 2c2163b1a01b1bcddc24d479b14c01f2ec8fcecd Mon Sep 17 00:00:00 2001 From: caballeto Date: Sun, 19 Apr 2026 19:51:04 +0200 Subject: [PATCH 03/10] chore: add spec-check workflow for OpenAPI drift detection Made-with: Cursor --- .github/workflows/spec-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spec-check.yml b/.github/workflows/spec-check.yml index 5fa9a14..435938d 100644 --- a/.github/workflows/spec-check.yml +++ b/.github/workflows/spec-check.yml @@ -23,7 +23,7 @@ jobs: - name: Download latest OpenAPI spec from monorepo run: | - gh api repos/devhelmhq/mono/contents/docs/openapi/monitoring-api.yaml \ + gh api repos/devhelmhq/mono/contents/docs/openapi/monitoring-api.json \ -H "Accept: application/vnd.github.raw+json" \ -o docs/openapi/monitoring-api.json env: From 997706f129e34f6f5ced16b52ee201991ace4f5b Mon Sep 17 00:00:00 2001 From: caballeto Date: Sun, 19 Apr 2026 20:00:54 +0200 Subject: [PATCH 04/10] fix: replace Any types in _http.py and add descriptive enum aliases - Replace all Any return/param types in _http.py with concrete types (END-1078) - Remove dead unwrap_single function - Add 31 descriptive enum aliases in types.py (END-1079) - Re-export all enum aliases from __init__.py for stable public API Made-with: Cursor --- src/devhelm/__init__.py | 63 +++++++++++++++ src/devhelm/_http.py | 34 ++++---- src/devhelm/_pagination.py | 8 +- src/devhelm/types.py | 158 +++++++++++++++++++++++++++++++++++++ tests/test_http.py | 22 +----- 5 files changed, 246 insertions(+), 39 deletions(-) diff --git a/src/devhelm/__init__.py b/src/devhelm/__init__.py index 52d51cb..0588ab5 100644 --- a/src/devhelm/__init__.py +++ b/src/devhelm/__init__.py @@ -22,11 +22,15 @@ AddCustomDomainRequest, AddResourceGroupMemberRequest, AdminAddSubscriberRequest, + AffectedComponentStatus, AlertChannelDto, + AlertDeliveryStatus, ApiKeyCreateResponse, ApiKeyDto, + AssertionSeverity, AssertionTestResultDto, CheckResultDto, + ConfirmationPolicyType, CreateAlertChannelRequest, CreateApiKeyRequest, CreateEnvironmentRequest, @@ -42,32 +46,59 @@ CreateStatusPageRequest, CreateTagRequest, CreateWebhookEndpointRequest, + CustomDomainStatus, DashboardOverviewDto, DeployLockDto, EnvironmentDto, IncidentDetailDto, IncidentDto, + IncidentNewStatus, + IncidentOldStatus, + IncidentSeverity, + IncidentStatus, + IncidentUpdateCreatedBy, + LinkedIncidentStatus, + MemberStatus, + MembershipStatus, + MonitorAssertionSeverity, + MonitorCurrentStatus, MonitorDto, + MonitorDtoType, + MonitorType, MonitorVersionDto, + NotificationDispatchStatus, NotificationPolicyDto, + PublishIncidentStatus, ReorderComponentsRequest, ResolveIncidentRequest, ResourceGroupDto, + ResourceGroupHealthStatus, ResourceGroupMemberDto, SecretDto, ServiceSubscriptionDto, StatusPageBranding, + StatusPageComponentCurrentStatus, StatusPageComponentDto, + StatusPageComponentDtoType, StatusPageComponentGroupDto, + StatusPageComponentType, StatusPageCustomDomainDto, StatusPageDto, StatusPageIncidentComponentDto, + StatusPageIncidentComponentStatus, StatusPageIncidentDto, + StatusPageIncidentStatus, StatusPageIncidentUpdateDto, + StatusPageOverallStatus, StatusPageSubscriberDto, + StatusPageUpdateCreatedBy, + StatusPageUpdateStatus, TagDto, TestChannelResult, + TriggerRuleSeverity, + TriggerRuleType, UpdateAlertChannelRequest, + UpdateAssertionSeverity, UpdateEnvironmentRequest, UpdateMonitorRequest, UpdateNotificationPolicyRequest, @@ -172,4 +203,36 @@ "UpdateWebhookEndpointRequest", "CreateApiKeyRequest", "AcquireDeployLockRequest", + # Enum aliases (descriptive names for codegen-numbered enums) + "AffectedComponentStatus", + "AlertDeliveryStatus", + "AssertionSeverity", + "ConfirmationPolicyType", + "CustomDomainStatus", + "IncidentNewStatus", + "IncidentOldStatus", + "IncidentSeverity", + "IncidentStatus", + "IncidentUpdateCreatedBy", + "LinkedIncidentStatus", + "MemberStatus", + "MembershipStatus", + "MonitorAssertionSeverity", + "MonitorCurrentStatus", + "MonitorDtoType", + "MonitorType", + "NotificationDispatchStatus", + "PublishIncidentStatus", + "ResourceGroupHealthStatus", + "StatusPageComponentCurrentStatus", + "StatusPageComponentDtoType", + "StatusPageComponentType", + "StatusPageIncidentComponentStatus", + "StatusPageIncidentStatus", + "StatusPageOverallStatus", + "StatusPageUpdateCreatedBy", + "StatusPageUpdateStatus", + "TriggerRuleSeverity", + "TriggerRuleType", + "UpdateAssertionSeverity", ] diff --git a/src/devhelm/_http.py b/src/devhelm/_http.py index 992ecfa..e5593a2 100644 --- a/src/devhelm/_http.py +++ b/src/devhelm/_http.py @@ -64,7 +64,12 @@ def path_param(value: str | int) -> str: return quote(str(value), safe="") -def _serialize_body(body: Any) -> Any: +_JsonResponse = dict[str, object] | list[object] | None + + +def _serialize_body( + body: BaseModel | dict[str, object] | None, +) -> dict[str, object] | None: """Convert a Pydantic model to a JSON-serializable dict. Rejects raw dicts to prevent callers from bypassing Pydantic @@ -82,39 +87,40 @@ def _serialize_body(body: Any) -> Any: return body -def checked_fetch(response: httpx.Response) -> Any: +def checked_fetch(response: httpx.Response) -> _JsonResponse: """Check an httpx response and raise DevhelmError on failure.""" if response.is_success: if response.status_code == 204: return None - return response.json() + return response.json() # type: ignore[no-any-return] raise error_from_response(response.status_code, response.text) -def unwrap_single(resp: Any) -> Any: - """Unwrap a SingleValueResponse envelope: {data: T} -> T.""" - if isinstance(resp, dict) and "data" in resp: - return resp["data"] - return resp - - def api_get( client: httpx.Client, path: str, params: dict[str, Any] | None = None -) -> Any: +) -> _JsonResponse: return checked_fetch(client.get(path, params=params)) -def api_post(client: httpx.Client, path: str, body: Any = None) -> Any: +def api_post( + client: httpx.Client, + path: str, + body: BaseModel | dict[str, object] | None = None, +) -> _JsonResponse: if body is None: return checked_fetch(client.post(path)) return checked_fetch(client.post(path, json=_serialize_body(body))) -def api_put(client: httpx.Client, path: str, body: Any) -> Any: +def api_put( + client: httpx.Client, path: str, body: BaseModel | dict[str, object] | None +) -> _JsonResponse: return checked_fetch(client.put(path, json=_serialize_body(body))) -def api_patch(client: httpx.Client, path: str, body: Any) -> Any: +def api_patch( + client: httpx.Client, path: str, body: BaseModel | dict[str, object] | None +) -> _JsonResponse: return checked_fetch(client.patch(path, json=_serialize_body(body))) diff --git a/src/devhelm/_pagination.py b/src/devhelm/_pagination.py index bfe7c25..6396836 100644 --- a/src/devhelm/_pagination.py +++ b/src/devhelm/_pagination.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any, Generic, TypeVar +from typing import Any, Generic, TypeVar, cast import httpx from pydantic import BaseModel @@ -66,8 +66,8 @@ def fetch_page( data=items, has_next=bool(resp.get("hasNext")) if isinstance(resp, dict) else False, has_prev=bool(resp.get("hasPrev")) if isinstance(resp, dict) else False, - total_elements=resp.get("totalElements") if isinstance(resp, dict) else None, - total_pages=resp.get("totalPages") if isinstance(resp, dict) else None, + total_elements=cast(int | None, resp.get("totalElements")) if isinstance(resp, dict) else None, + total_pages=cast(int | None, resp.get("totalPages")) if isinstance(resp, dict) else None, ) @@ -90,6 +90,6 @@ def fetch_cursor_page( items = parse_list(model_class, raw_items, f"GET {path}") return CursorPage( data=items, - next_cursor=(resp.get("nextCursor") if isinstance(resp, dict) else None), + next_cursor=cast(str | None, resp.get("nextCursor")) if isinstance(resp, dict) else None, has_more=bool(resp.get("hasMore")) if isinstance(resp, dict) else False, ) diff --git a/src/devhelm/types.py b/src/devhelm/types.py index 937b7ab..e9c7df9 100644 --- a/src/devhelm/types.py +++ b/src/devhelm/types.py @@ -3,11 +3,31 @@ These are the stable public names consumers should import from `devhelm`. The underlying `_generated` module is auto-generated by `datamodel-codegen` from the vendored OpenAPI spec; do not import from it directly. + +Enum Alias Mapping +================== +``datamodel-codegen`` produces numbered suffixes (Status1 … Status15, Type1 … Type6, +etc.) when multiple OpenAPI schemas share the same enum name. The aliases below +give each generated enum a descriptive, context-aware name based on the DTO that +references it. + +When ``_generated.py`` is regenerated the numbering may shift. Verify aliases by +checking which DTO class references each generated enum name, then update the +imports here to match. + +Duplicate-value groups (same members, different Python classes): + StatusPageIncidentStatus ≡ LinkedIncidentStatus ≡ PublishIncidentStatus ≡ StatusPageUpdateStatus + MembershipStatus ≡ MemberStatus + AssertionSeverity ≡ MonitorAssertionSeverity ≡ UpdateAssertionSeverity + MonitorType ≡ MonitorDtoType + StatusPageComponentType ≡ StatusPageComponentDtoType + IncidentUpdateCreatedBy ≡ StatusPageUpdateCreatedBy """ from __future__ import annotations from devhelm._generated import ( + # ── DTOs ────────────────────────────────────────────────────────────── AcquireDeployLockRequest, AddCustomDomainRequest, AddResourceGroupMemberRequest, @@ -71,9 +91,82 @@ UpdateWebhookEndpointRequest, WebhookEndpointDto, WebhookTestResult, + # ── Enums: unique names (no suffix collisions) ──────────────────────── + Action, + AggregationType, + AlertSensitivity, + AssertionType, + AuthType, + ChangedVia, + ChannelType, + CompletionReason, + EventType, + HealthThresholdType, + Impact, + IncidentMode, + ManagedBy, + Method, + Operator, + OrgRole, + RecordType, + ResolutionReason, + RoleOffered, + Scope, + Source, + ThresholdStatus, + Tier, + TierAvailability, + VerificationMethod, + Visibility, + # ── Enums: ambiguous generated names → descriptive aliases ──────────── + # + # Status enums + Status as AffectedComponentStatus, # AffectedComponent.status + Status1 as AlertDeliveryStatus, # AlertDeliveryDto.status + Status2 as MembershipStatus, # ChangeStatusRequest.status + Status3 as StatusPageIncidentStatus, # CreateStatusPageIncidentRequest.status + Status6 as IncidentStatus, # IncidentDto.status + Status8 as LinkedIncidentStatus, # LinkedStatusPageIncidentDto.status + Status9 as MemberStatus, # MemberDto.status + Status10 as NotificationDispatchStatus, # NotificationDispatchDto.status + Status11 as PublishIncidentStatus, # PublishStatusPageIncidentRequest.status + Status12 as ResourceGroupHealthStatus, # ResourceGroupHealthDto.status + Status14 as CustomDomainStatus, # StatusPageCustomDomainDto.status + Status15 as StatusPageUpdateStatus, # StatusPageIncidentUpdateDto.status + # + # Severity enums + Severity as AssertionSeverity, # AssertionResultDto.severity + Severity3 as IncidentSeverity, # CreateManualIncidentRequest.severity + Severity6 as MonitorAssertionSeverity, # MonitorAssertionDto.severity + Severity7 as TriggerRuleSeverity, # TriggerRule.severity + Severity8 as UpdateAssertionSeverity, # UpdateAssertionRequest.severity + # + # Type enums + Type as ConfirmationPolicyType, # ConfirmationPolicy.type + Type1 as MonitorType, # CreateMonitorRequest.type + Type2 as StatusPageComponentType, # CreateStatusPageComponentRequest.type + Type3 as MonitorDtoType, # MonitorDto.type + Type5 as StatusPageComponentDtoType, # StatusPageComponentDto.type + Type6 as TriggerRuleType, # TriggerRule.type + # + # CurrentStatus enums + CurrentStatus as MonitorCurrentStatus, # ResultSummaryDto.current_status + CurrentStatus1 as StatusPageComponentCurrentStatus, # StatusPageComponentDto.current_status + # + # CreatedBy enums + CreatedBy as IncidentUpdateCreatedBy, # IncidentUpdateDto.created_by + CreatedBy1 as StatusPageUpdateCreatedBy, # StatusPageIncidentUpdateDto.created_by + # + # NewStatus / OldStatus / OverallStatus / ComponentStatus — already + # semi-descriptive but aliased for consistency with the rest of the SDK. + NewStatus as IncidentNewStatus, # AddIncidentUpdateRequest.new_status + OldStatus as IncidentOldStatus, # IncidentUpdateDto.old_status + OverallStatus as StatusPageOverallStatus, # StatusPageDto.overall_status + ComponentStatus as StatusPageIncidentComponentStatus, # StatusPageIncidentComponentDto.component_status ) __all__ = [ + # ── DTOs ────────────────────────────────────────────────────────────── "AcquireDeployLockRequest", "AddCustomDomainRequest", "AddResourceGroupMemberRequest", @@ -137,4 +230,69 @@ "UpdateWebhookEndpointRequest", "WebhookEndpointDto", "WebhookTestResult", + # ── Enums: unique names ─────────────────────────────────────────────── + "Action", + "AggregationType", + "AlertSensitivity", + "AssertionType", + "AuthType", + "ChangedVia", + "ChannelType", + "CompletionReason", + "EventType", + "HealthThresholdType", + "Impact", + "IncidentMode", + "ManagedBy", + "Method", + "Operator", + "OrgRole", + "RecordType", + "ResolutionReason", + "RoleOffered", + "Scope", + "Source", + "ThresholdStatus", + "Tier", + "TierAvailability", + "VerificationMethod", + "Visibility", + # ── Enums: descriptive aliases for ambiguous generated names ────────── + # Status + "AffectedComponentStatus", + "AlertDeliveryStatus", + "CustomDomainStatus", + "IncidentStatus", + "LinkedIncidentStatus", + "MemberStatus", + "MembershipStatus", + "NotificationDispatchStatus", + "PublishIncidentStatus", + "ResourceGroupHealthStatus", + "StatusPageIncidentStatus", + "StatusPageUpdateStatus", + # Severity + "AssertionSeverity", + "IncidentSeverity", + "MonitorAssertionSeverity", + "TriggerRuleSeverity", + "UpdateAssertionSeverity", + # Type + "ConfirmationPolicyType", + "MonitorDtoType", + "MonitorType", + "StatusPageComponentDtoType", + "StatusPageComponentType", + "TriggerRuleType", + # CurrentStatus + "MonitorCurrentStatus", + "StatusPageComponentCurrentStatus", + # CreatedBy + "IncidentUpdateCreatedBy", + "StatusPageUpdateCreatedBy", + # Already semi-descriptive, aliased for consistency + "IncidentNewStatus", + "IncidentOldStatus", + "StatusPageIncidentComponentStatus", + "StatusPageOverallStatus", ] diff --git a/tests/test_http.py b/tests/test_http.py index 344377a..742502c 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -6,29 +6,9 @@ from pydantic import BaseModel, Field from devhelm._errors import DevhelmError -from devhelm._http import DevhelmConfig, build_client, path_param, unwrap_single +from devhelm._http import DevhelmConfig, build_client, path_param from devhelm._validation import parse_list, parse_model, parse_single -# ---------- unwrap_single (raw JSON envelope) ---------- - - -class TestUnwrapSingle: - def test_unwraps_data_key(self) -> None: - assert unwrap_single({"data": {"id": 1}}) == {"id": 1} - - def test_returns_bare_value(self) -> None: - assert unwrap_single({"id": 1}) == {"id": 1} - - def test_unwraps_null_data(self) -> None: - assert unwrap_single({"data": None}) is None - - def test_unwraps_nested_data(self) -> None: - result = unwrap_single({"data": {"data": "inner"}}) - assert result == {"data": "inner"} - - def test_non_dict_passthrough(self) -> None: - assert unwrap_single([1, 2, 3]) == [1, 2, 3] - # ---------- path_param ---------- From ad24b033c6a6c616b461f0ad4f37722e9a2dfb88 Mon Sep 17 00:00:00 2001 From: caballeto Date: Sun, 19 Apr 2026 23:47:59 +0200 Subject: [PATCH 05/10] feat(typing): widen body params with RequestBody + add spec parity tests Resolves END-1162. Resource method signatures previously accepted only the typed Pydantic model (e.g. CreateMonitorRequest), but the runtime ``validate_request`` helper coerces dicts via ``model_validate`` and the README/client docstrings show dict-style usage. Under ``mypy --strict`` those examples were type errors. Introduce ``RequestBody[T] = T | Mapping[str, Any]`` in ``_validation`` and apply it to every mutating resource method body parameter so dict literals type-check while typed model instances remain a first-class option. Re-export ``RequestBody`` and the previously omitted ``AddIncidentUpdateRequest`` from the package root and ``types`` module. Add ``tests/test_spec_parity.py`` to lock the SDK against logical drift from ``docs/openapi/monitoring-api.json``: * every public DTO must exist in the spec (catches removals/renames), * every request DTO's required fields must be a superset of the spec's, * every mutating method body annotation must include ``Mapping[str, Any]`` (catches future regressions to the dict ergonomics fix), and * every top-level path the SDK calls must exist in the spec. All 681 tests pass; ``mypy --strict src/`` is clean; manual smoke check confirms dict-style ``client.monitors.create({...})`` now type-checks. Made-with: Cursor --- src/devhelm/__init__.py | 7 +- src/devhelm/_validation.py | 15 +- src/devhelm/resources/alert_channels.py | 8 +- src/devhelm/resources/api_keys.py | 4 +- src/devhelm/resources/deploy_lock.py | 4 +- src/devhelm/resources/environments.py | 8 +- src/devhelm/resources/incidents.py | 8 +- src/devhelm/resources/monitors.py | 8 +- .../resources/notification_policies.py | 8 +- src/devhelm/resources/resource_groups.py | 8 +- src/devhelm/resources/secrets.py | 6 +- src/devhelm/resources/status_pages.py | 32 +- src/devhelm/resources/tags.py | 6 +- src/devhelm/resources/webhooks.py | 8 +- src/devhelm/types.py | 160 +++++--- tests/test_spec_parity.py | 384 ++++++++++++++++++ 16 files changed, 570 insertions(+), 104 deletions(-) create mode 100644 tests/test_spec_parity.py diff --git a/src/devhelm/__init__.py b/src/devhelm/__init__.py index 0588ab5..a41cec7 100644 --- a/src/devhelm/__init__.py +++ b/src/devhelm/__init__.py @@ -2,6 +2,7 @@ from devhelm._errors import AuthError, DevhelmError from devhelm._pagination import CursorPage, Page +from devhelm._validation import RequestBody from devhelm.client import Devhelm from devhelm.resources.alert_channels import AlertChannels from devhelm.resources.api_keys import ApiKeys @@ -20,6 +21,7 @@ from devhelm.types import ( AcquireDeployLockRequest, AddCustomDomainRequest, + AddIncidentUpdateRequest, AddResourceGroupMemberRequest, AdminAddSubscriberRequest, AffectedComponentStatus, @@ -58,8 +60,8 @@ IncidentStatus, IncidentUpdateCreatedBy, LinkedIncidentStatus, - MemberStatus, MembershipStatus, + MemberStatus, MonitorAssertionSeverity, MonitorCurrentStatus, MonitorDto, @@ -123,6 +125,8 @@ # Pagination "Page", "CursorPage", + # Typing helpers + "RequestBody", # Resource classes "Monitors", "Incidents", @@ -170,6 +174,7 @@ "TestChannelResult", "WebhookTestResult", # Request types + "AddIncidentUpdateRequest", "CreateStatusPageRequest", "UpdateStatusPageRequest", "CreateStatusPageComponentRequest", diff --git a/src/devhelm/_validation.py b/src/devhelm/_validation.py index 7ca2e97..408238a 100644 --- a/src/devhelm/_validation.py +++ b/src/devhelm/_validation.py @@ -6,7 +6,8 @@ from __future__ import annotations -from typing import Any, TypeVar +from collections.abc import Mapping +from typing import Any, TypeAlias, TypeVar, Union from pydantic import BaseModel, TypeAdapter, ValidationError @@ -14,8 +15,18 @@ M = TypeVar("M", bound=BaseModel) +# Public type alias used in resource method signatures. Every "create"/ +# "update" body parameter is annotated as ``RequestBody[]`` so that +# both fully-typed model instances *and* plain dicts (the most ergonomic +# form, and the one shown throughout the README) type-check under +# ``mypy --strict``. ``validate_request`` normalises both forms before +# any HTTP call is made. +RequestBody: TypeAlias = Union[M, Mapping[str, Any]] -def validate_request(model_class: type[M], body: Any, context: str = "") -> M: + +def validate_request( + model_class: type[M], body: RequestBody[M], context: str = "" +) -> M: """Validate a request body against its Pydantic model before sending. If *body* is already an instance of *model_class* it passes through. diff --git a/src/devhelm/resources/alert_channels.py b/src/devhelm/resources/alert_channels.py index ca9f592..07da2ef 100644 --- a/src/devhelm/resources/alert_channels.py +++ b/src/devhelm/resources/alert_channels.py @@ -10,7 +10,7 @@ ) from devhelm._http import api_delete, api_get, api_post, api_put, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single, validate_request +from devhelm._validation import RequestBody, parse_single, validate_request class AlertChannels: @@ -37,7 +37,7 @@ def get(self, id: int | str) -> AlertChannelDto: f"GET /api/v1/alert-channels/{id}", ) - def create(self, body: CreateAlertChannelRequest) -> AlertChannelDto: + def create(self, body: RequestBody[CreateAlertChannelRequest]) -> AlertChannelDto: """Create a new alert channel.""" body = validate_request(CreateAlertChannelRequest, body, "alertChannels.create") return parse_single( @@ -46,7 +46,9 @@ def create(self, body: CreateAlertChannelRequest) -> AlertChannelDto: "POST /api/v1/alert-channels", ) - def update(self, id: int | str, body: UpdateAlertChannelRequest) -> AlertChannelDto: + def update( + self, id: int | str, body: RequestBody[UpdateAlertChannelRequest] + ) -> AlertChannelDto: """Update an alert channel.""" body = validate_request(UpdateAlertChannelRequest, body, "alertChannels.update") return parse_single( diff --git a/src/devhelm/resources/api_keys.py b/src/devhelm/resources/api_keys.py index c485aeb..ec40991 100644 --- a/src/devhelm/resources/api_keys.py +++ b/src/devhelm/resources/api_keys.py @@ -5,7 +5,7 @@ from devhelm._generated import ApiKeyCreateResponse, ApiKeyDto, CreateApiKeyRequest from devhelm._http import api_delete, api_post, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single, validate_request +from devhelm._validation import RequestBody, parse_single, validate_request class ApiKeys: @@ -22,7 +22,7 @@ def list_page(self, page: int, size: int) -> Page[ApiKeyDto]: """List API keys with manual page control.""" return fetch_page(self._client, "/api/v1/api-keys", ApiKeyDto, page, size) - def create(self, body: CreateApiKeyRequest) -> ApiKeyCreateResponse: + def create(self, body: RequestBody[CreateApiKeyRequest]) -> ApiKeyCreateResponse: """Create an API key. Returns the key value (shown only once).""" body = validate_request(CreateApiKeyRequest, body, "apiKeys.create") return parse_single( diff --git a/src/devhelm/resources/deploy_lock.py b/src/devhelm/resources/deploy_lock.py index 98d4ea7..d95dd71 100644 --- a/src/devhelm/resources/deploy_lock.py +++ b/src/devhelm/resources/deploy_lock.py @@ -4,7 +4,7 @@ from devhelm._generated import AcquireDeployLockRequest, DeployLockDto from devhelm._http import api_delete, api_get, api_post, path_param -from devhelm._validation import parse_model, parse_single, validate_request +from devhelm._validation import RequestBody, parse_model, parse_single, validate_request class DeployLock: @@ -13,7 +13,7 @@ class DeployLock: def __init__(self, client: httpx.Client) -> None: self._client = client - def acquire(self, body: AcquireDeployLockRequest) -> DeployLockDto: + def acquire(self, body: RequestBody[AcquireDeployLockRequest]) -> DeployLockDto: """Acquire a deploy lock.""" body = validate_request(AcquireDeployLockRequest, body, "deployLock.acquire") return parse_single( diff --git a/src/devhelm/resources/environments.py b/src/devhelm/resources/environments.py index b12d38c..6ae1fab 100644 --- a/src/devhelm/resources/environments.py +++ b/src/devhelm/resources/environments.py @@ -9,7 +9,7 @@ ) from devhelm._http import api_delete, api_get, api_post, api_put, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single, validate_request +from devhelm._validation import RequestBody, parse_single, validate_request class Environments: @@ -36,7 +36,7 @@ def get(self, slug: str) -> EnvironmentDto: f"GET /api/v1/environments/{slug}", ) - def create(self, body: CreateEnvironmentRequest) -> EnvironmentDto: + def create(self, body: RequestBody[CreateEnvironmentRequest]) -> EnvironmentDto: """Create an environment.""" body = validate_request(CreateEnvironmentRequest, body, "environments.create") return parse_single( @@ -45,7 +45,9 @@ def create(self, body: CreateEnvironmentRequest) -> EnvironmentDto: "POST /api/v1/environments", ) - def update(self, slug: str, body: UpdateEnvironmentRequest) -> EnvironmentDto: + def update( + self, slug: str, body: RequestBody[UpdateEnvironmentRequest] + ) -> EnvironmentDto: """Update an environment.""" body = validate_request(UpdateEnvironmentRequest, body, "environments.update") return parse_single( diff --git a/src/devhelm/resources/incidents.py b/src/devhelm/resources/incidents.py index b0c234e..fb05577 100644 --- a/src/devhelm/resources/incidents.py +++ b/src/devhelm/resources/incidents.py @@ -10,7 +10,7 @@ ) from devhelm._http import api_delete, api_get, api_post, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single, validate_request +from devhelm._validation import RequestBody, parse_single, validate_request class Incidents: @@ -35,7 +35,9 @@ def get(self, id: int | str) -> IncidentDetailDto: f"GET /api/v1/incidents/{id}", ) - def create(self, body: CreateManualIncidentRequest) -> IncidentDetailDto: + def create( + self, body: RequestBody[CreateManualIncidentRequest] + ) -> IncidentDetailDto: """Create a manual incident.""" body = validate_request(CreateManualIncidentRequest, body, "incidents.create") return parse_single( @@ -45,7 +47,7 @@ def create(self, body: CreateManualIncidentRequest) -> IncidentDetailDto: ) def resolve( - self, id: int | str, body: ResolveIncidentRequest | None = None + self, id: int | str, body: RequestBody[ResolveIncidentRequest] | None = None ) -> IncidentDetailDto: """Resolve an incident.""" if body is not None: diff --git a/src/devhelm/resources/monitors.py b/src/devhelm/resources/monitors.py index 064b0cf..afbc917 100644 --- a/src/devhelm/resources/monitors.py +++ b/src/devhelm/resources/monitors.py @@ -18,7 +18,7 @@ fetch_cursor_page, fetch_page, ) -from devhelm._validation import parse_single, validate_request +from devhelm._validation import RequestBody, parse_single, validate_request class Monitors: @@ -43,7 +43,7 @@ def get(self, id: int | str) -> MonitorDto: f"GET /api/v1/monitors/{id}", ) - def create(self, body: CreateMonitorRequest) -> MonitorDto: + def create(self, body: RequestBody[CreateMonitorRequest]) -> MonitorDto: """Create a new monitor.""" body = validate_request(CreateMonitorRequest, body, "monitors.create") return parse_single( @@ -52,7 +52,9 @@ def create(self, body: CreateMonitorRequest) -> MonitorDto: "POST /api/v1/monitors", ) - def update(self, id: int | str, body: UpdateMonitorRequest) -> MonitorDto: + def update( + self, id: int | str, body: RequestBody[UpdateMonitorRequest] + ) -> MonitorDto: """Update an existing monitor.""" body = validate_request(UpdateMonitorRequest, body, "monitors.update") return parse_single( diff --git a/src/devhelm/resources/notification_policies.py b/src/devhelm/resources/notification_policies.py index a67a1de..0ec6bf1 100644 --- a/src/devhelm/resources/notification_policies.py +++ b/src/devhelm/resources/notification_policies.py @@ -9,7 +9,7 @@ ) from devhelm._http import api_delete, api_get, api_post, api_put, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single, validate_request +from devhelm._validation import RequestBody, parse_single, validate_request class NotificationPolicies: @@ -42,7 +42,9 @@ def get(self, id: int | str) -> NotificationPolicyDto: f"GET /api/v1/notification-policies/{id}", ) - def create(self, body: CreateNotificationPolicyRequest) -> NotificationPolicyDto: + def create( + self, body: RequestBody[CreateNotificationPolicyRequest] + ) -> NotificationPolicyDto: """Create a notification policy.""" body = validate_request( CreateNotificationPolicyRequest, body, "notificationPolicies.create" @@ -54,7 +56,7 @@ def create(self, body: CreateNotificationPolicyRequest) -> NotificationPolicyDto ) def update( - self, id: int | str, body: UpdateNotificationPolicyRequest + self, id: int | str, body: RequestBody[UpdateNotificationPolicyRequest] ) -> NotificationPolicyDto: """Update a notification policy.""" body = validate_request( diff --git a/src/devhelm/resources/resource_groups.py b/src/devhelm/resources/resource_groups.py index ef55e52..ee13cc2 100644 --- a/src/devhelm/resources/resource_groups.py +++ b/src/devhelm/resources/resource_groups.py @@ -11,7 +11,7 @@ ) from devhelm._http import api_delete, api_get, api_post, api_put, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single, validate_request +from devhelm._validation import RequestBody, parse_single, validate_request class ResourceGroups: @@ -40,7 +40,7 @@ def get(self, id: int | str) -> ResourceGroupDto: f"GET /api/v1/resource-groups/{id}", ) - def create(self, body: CreateResourceGroupRequest) -> ResourceGroupDto: + def create(self, body: RequestBody[CreateResourceGroupRequest]) -> ResourceGroupDto: """Create a resource group.""" body = validate_request( CreateResourceGroupRequest, body, "resourceGroups.create" @@ -52,7 +52,7 @@ def create(self, body: CreateResourceGroupRequest) -> ResourceGroupDto: ) def update( - self, id: int | str, body: UpdateResourceGroupRequest + self, id: int | str, body: RequestBody[UpdateResourceGroupRequest] ) -> ResourceGroupDto: """Update a resource group.""" body = validate_request( @@ -69,7 +69,7 @@ def delete(self, id: int | str) -> None: api_delete(self._client, f"/api/v1/resource-groups/{path_param(id)}") def add_member( - self, group_id: int | str, body: AddResourceGroupMemberRequest + self, group_id: int | str, body: RequestBody[AddResourceGroupMemberRequest] ) -> ResourceGroupMemberDto: """Add a member to a resource group.""" body = validate_request( diff --git a/src/devhelm/resources/secrets.py b/src/devhelm/resources/secrets.py index adddaf9..d9ad97d 100644 --- a/src/devhelm/resources/secrets.py +++ b/src/devhelm/resources/secrets.py @@ -5,7 +5,7 @@ from devhelm._generated import CreateSecretRequest, SecretDto, UpdateSecretRequest from devhelm._http import api_delete, api_post, api_put, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single, validate_request +from devhelm._validation import RequestBody, parse_single, validate_request class Secrets: @@ -22,7 +22,7 @@ def list_page(self, page: int, size: int) -> Page[SecretDto]: """List secrets with manual page control.""" return fetch_page(self._client, "/api/v1/secrets", SecretDto, page, size) - def create(self, body: CreateSecretRequest) -> SecretDto: + def create(self, body: RequestBody[CreateSecretRequest]) -> SecretDto: """Create a secret.""" body = validate_request(CreateSecretRequest, body, "secrets.create") return parse_single( @@ -31,7 +31,7 @@ def create(self, body: CreateSecretRequest) -> SecretDto: "POST /api/v1/secrets", ) - def update(self, key: str, body: UpdateSecretRequest) -> SecretDto: + def update(self, key: str, body: RequestBody[UpdateSecretRequest]) -> SecretDto: """Update a secret by key.""" body = validate_request(UpdateSecretRequest, body, "secrets.update") return parse_single( diff --git a/src/devhelm/resources/status_pages.py b/src/devhelm/resources/status_pages.py index 5b7f243..e066d93 100644 --- a/src/devhelm/resources/status_pages.py +++ b/src/devhelm/resources/status_pages.py @@ -24,7 +24,7 @@ ) from devhelm._http import api_delete, api_get, api_post, api_put, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single, validate_request +from devhelm._validation import RequestBody, parse_single, validate_request _BASE = "/api/v1/status-pages" @@ -46,7 +46,7 @@ def list(self, page_id: int | str) -> list[StatusPageComponentDto]: ) def create( - self, page_id: int | str, body: CreateStatusPageComponentRequest + self, page_id: int | str, body: RequestBody[CreateStatusPageComponentRequest] ) -> StatusPageComponentDto: """Add a component to a status page.""" body = validate_request( @@ -62,7 +62,7 @@ def update( self, page_id: int | str, component_id: int | str, - body: UpdateStatusPageComponentRequest, + body: RequestBody[UpdateStatusPageComponentRequest], ) -> StatusPageComponentDto: """Update a component.""" body = validate_request( @@ -84,7 +84,9 @@ def delete(self, page_id: int | str, component_id: int | str) -> None: self._client, f"{_page_path(page_id)}/components/{path_param(component_id)}" ) - def reorder(self, page_id: int | str, body: ReorderComponentsRequest) -> None: + def reorder( + self, page_id: int | str, body: RequestBody[ReorderComponentsRequest] + ) -> None: """Batch reorder components.""" body = validate_request( ReorderComponentsRequest, body, "statusPages.components.reorder" @@ -105,7 +107,9 @@ def list(self, page_id: int | str) -> list[StatusPageComponentGroupDto]: ) def create( - self, page_id: int | str, body: CreateStatusPageComponentGroupRequest + self, + page_id: int | str, + body: RequestBody[CreateStatusPageComponentGroupRequest], ) -> StatusPageComponentGroupDto: """Create a component group.""" body = validate_request( @@ -121,7 +125,7 @@ def update( self, page_id: int | str, group_id: int | str, - body: UpdateStatusPageComponentGroupRequest, + body: RequestBody[UpdateStatusPageComponentGroupRequest], ) -> StatusPageComponentGroupDto: """Update a component group.""" body = validate_request( @@ -172,7 +176,7 @@ def get(self, page_id: int | str, incident_id: int | str) -> StatusPageIncidentD ) def create( - self, page_id: int | str, body: CreateStatusPageIncidentRequest + self, page_id: int | str, body: RequestBody[CreateStatusPageIncidentRequest] ) -> StatusPageIncidentDto: """Create a status page incident.""" body = validate_request( @@ -188,7 +192,7 @@ def update( self, page_id: int | str, incident_id: int | str, - body: UpdateStatusPageIncidentRequest, + body: RequestBody[UpdateStatusPageIncidentRequest], ) -> StatusPageIncidentDto: """Update an incident.""" body = validate_request( @@ -208,7 +212,7 @@ def post_update( self, page_id: int | str, incident_id: int | str, - body: CreateStatusPageIncidentUpdateRequest, + body: RequestBody[CreateStatusPageIncidentUpdateRequest], ) -> StatusPageIncidentDto: """Post a timeline update on an incident.""" body = validate_request( @@ -272,7 +276,7 @@ def list( ) def add( - self, page_id: int | str, body: AdminAddSubscriberRequest + self, page_id: int | str, body: RequestBody[AdminAddSubscriberRequest] ) -> StatusPageSubscriberDto: """Add a subscriber (admin).""" body = validate_request( @@ -305,7 +309,7 @@ def list(self, page_id: int | str) -> list[StatusPageCustomDomainDto]: ) def add( - self, page_id: int | str, body: AddCustomDomainRequest + self, page_id: int | str, body: RequestBody[AddCustomDomainRequest] ) -> StatusPageCustomDomainDto: """Add a custom domain.""" body = validate_request(AddCustomDomainRequest, body, "statusPages.domains.add") @@ -365,7 +369,7 @@ def get(self, id: int | str) -> StatusPageDto: f"GET {_page_path(id)}", ) - def create(self, body: CreateStatusPageRequest) -> StatusPageDto: + def create(self, body: RequestBody[CreateStatusPageRequest]) -> StatusPageDto: """Create a status page.""" body = validate_request(CreateStatusPageRequest, body, "statusPages.create") return parse_single( @@ -374,7 +378,9 @@ def create(self, body: CreateStatusPageRequest) -> StatusPageDto: "POST /api/v1/status-pages", ) - def update(self, id: int | str, body: UpdateStatusPageRequest) -> StatusPageDto: + def update( + self, id: int | str, body: RequestBody[UpdateStatusPageRequest] + ) -> StatusPageDto: """Update a status page.""" body = validate_request(UpdateStatusPageRequest, body, "statusPages.update") return parse_single( diff --git a/src/devhelm/resources/tags.py b/src/devhelm/resources/tags.py index b2de157..02481dd 100644 --- a/src/devhelm/resources/tags.py +++ b/src/devhelm/resources/tags.py @@ -5,7 +5,7 @@ from devhelm._generated import CreateTagRequest, TagDto, UpdateTagRequest from devhelm._http import api_delete, api_get, api_post, api_put, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single, validate_request +from devhelm._validation import RequestBody, parse_single, validate_request class Tags: @@ -30,14 +30,14 @@ def get(self, id: int | str) -> TagDto: f"GET /api/v1/tags/{id}", ) - def create(self, body: CreateTagRequest) -> TagDto: + def create(self, body: RequestBody[CreateTagRequest]) -> TagDto: """Create a tag.""" body = validate_request(CreateTagRequest, body, "tags.create") return parse_single( TagDto, api_post(self._client, "/api/v1/tags", body), "POST /api/v1/tags" ) - def update(self, id: int | str, body: UpdateTagRequest) -> TagDto: + def update(self, id: int | str, body: RequestBody[UpdateTagRequest]) -> TagDto: """Update a tag.""" body = validate_request(UpdateTagRequest, body, "tags.update") return parse_single( diff --git a/src/devhelm/resources/webhooks.py b/src/devhelm/resources/webhooks.py index 171d118..2dc9a90 100644 --- a/src/devhelm/resources/webhooks.py +++ b/src/devhelm/resources/webhooks.py @@ -10,7 +10,7 @@ ) from devhelm._http import api_delete, api_get, api_post, api_put, path_param from devhelm._pagination import Page, fetch_all_pages, fetch_page -from devhelm._validation import parse_single, validate_request +from devhelm._validation import RequestBody, parse_single, validate_request class Webhooks: @@ -37,7 +37,9 @@ def get(self, id: int | str) -> WebhookEndpointDto: f"GET /api/v1/webhooks/{id}", ) - def create(self, body: CreateWebhookEndpointRequest) -> WebhookEndpointDto: + def create( + self, body: RequestBody[CreateWebhookEndpointRequest] + ) -> WebhookEndpointDto: """Create a webhook endpoint.""" body = validate_request(CreateWebhookEndpointRequest, body, "webhooks.create") return parse_single( @@ -47,7 +49,7 @@ def create(self, body: CreateWebhookEndpointRequest) -> WebhookEndpointDto: ) def update( - self, id: int | str, body: UpdateWebhookEndpointRequest + self, id: int | str, body: RequestBody[UpdateWebhookEndpointRequest] ) -> WebhookEndpointDto: """Update a webhook endpoint.""" body = validate_request(UpdateWebhookEndpointRequest, body, "webhooks.update") diff --git a/src/devhelm/types.py b/src/devhelm/types.py index e9c7df9..7a6d6b9 100644 --- a/src/devhelm/types.py +++ b/src/devhelm/types.py @@ -29,14 +29,24 @@ from devhelm._generated import ( # ── DTOs ────────────────────────────────────────────────────────────── AcquireDeployLockRequest, + # ── Enums: unique names (no suffix collisions) ──────────────────────── + Action, AddCustomDomainRequest, + AddIncidentUpdateRequest, AddResourceGroupMemberRequest, AdminAddSubscriberRequest, + AggregationType, AlertChannelDto, + AlertSensitivity, ApiKeyCreateResponse, ApiKeyDto, AssertionTestResultDto, + AssertionType, + AuthType, + ChangedVia, + ChannelType, CheckResultDto, # noqa: F401 + CompletionReason, CreateAlertChannelRequest, CreateApiKeyRequest, CreateEnvironmentRequest, @@ -55,17 +65,30 @@ DashboardOverviewDto, DeployLockDto, EnvironmentDto, + EventType, + HealthThresholdType, + Impact, IncidentDetailDto, IncidentDto, + IncidentMode, + ManagedBy, + Method, MonitorDto, MonitorVersionDto, NotificationPolicyDto, + Operator, + OrgRole, + RecordType, ReorderComponentsRequest, + ResolutionReason, ResolveIncidentRequest, ResourceGroupDto, ResourceGroupMemberDto, # noqa: F401 + RoleOffered, + Scope, SecretDto, ServiceSubscriptionDto, + Source, StatusPageBranding, StatusPageComponentDto, StatusPageComponentGroupDto, @@ -77,6 +100,9 @@ StatusPageSubscriberDto, TagDto, TestChannelResult, + ThresholdStatus, + Tier, + TierAvailability, UpdateAlertChannelRequest, UpdateEnvironmentRequest, UpdateMonitorRequest, @@ -89,86 +115,108 @@ UpdateStatusPageRequest, UpdateTagRequest, UpdateWebhookEndpointRequest, - WebhookEndpointDto, - WebhookTestResult, - # ── Enums: unique names (no suffix collisions) ──────────────────────── - Action, - AggregationType, - AlertSensitivity, - AssertionType, - AuthType, - ChangedVia, - ChannelType, - CompletionReason, - EventType, - HealthThresholdType, - Impact, - IncidentMode, - ManagedBy, - Method, - Operator, - OrgRole, - RecordType, - ResolutionReason, - RoleOffered, - Scope, - Source, - ThresholdStatus, - Tier, - TierAvailability, VerificationMethod, Visibility, + WebhookEndpointDto, + WebhookTestResult, +) +from devhelm._generated import ( + ComponentStatus as StatusPageIncidentComponentStatus, # StatusPageIncidentComponentDto.component_status +) +from devhelm._generated import ( + # + # CreatedBy enums + CreatedBy as IncidentUpdateCreatedBy, # IncidentUpdateDto.created_by +) +from devhelm._generated import ( + CreatedBy1 as StatusPageUpdateCreatedBy, # StatusPageIncidentUpdateDto.created_by +) +from devhelm._generated import ( + # + # CurrentStatus enums + CurrentStatus as MonitorCurrentStatus, # ResultSummaryDto.current_status +) +from devhelm._generated import ( + CurrentStatus1 as StatusPageComponentCurrentStatus, # StatusPageComponentDto.current_status +) +from devhelm._generated import ( + # + # NewStatus / OldStatus / OverallStatus / ComponentStatus — already + # semi-descriptive but aliased for consistency with the rest of the SDK. + NewStatus as IncidentNewStatus, # AddIncidentUpdateRequest.new_status +) +from devhelm._generated import ( + OldStatus as IncidentOldStatus, # IncidentUpdateDto.old_status +) +from devhelm._generated import ( + OverallStatus as StatusPageOverallStatus, # StatusPageDto.overall_status +) +from devhelm._generated import ( + # + # Severity enums + Severity as AssertionSeverity, # AssertionResultDto.severity +) +from devhelm._generated import ( + Severity3 as IncidentSeverity, # CreateManualIncidentRequest.severity +) +from devhelm._generated import ( + Severity6 as MonitorAssertionSeverity, # MonitorAssertionDto.severity +) +from devhelm._generated import Severity7 as TriggerRuleSeverity # TriggerRule.severity +from devhelm._generated import ( + Severity8 as UpdateAssertionSeverity, # UpdateAssertionRequest.severity +) +from devhelm._generated import ( # ── Enums: ambiguous generated names → descriptive aliases ──────────── # # Status enums Status as AffectedComponentStatus, # AffectedComponent.status - Status1 as AlertDeliveryStatus, # AlertDeliveryDto.status - Status2 as MembershipStatus, # ChangeStatusRequest.status +) +from devhelm._generated import Status1 as AlertDeliveryStatus # AlertDeliveryDto.status +from devhelm._generated import Status2 as MembershipStatus # ChangeStatusRequest.status +from devhelm._generated import ( Status3 as StatusPageIncidentStatus, # CreateStatusPageIncidentRequest.status - Status6 as IncidentStatus, # IncidentDto.status +) +from devhelm._generated import Status6 as IncidentStatus # IncidentDto.status +from devhelm._generated import ( Status8 as LinkedIncidentStatus, # LinkedStatusPageIncidentDto.status - Status9 as MemberStatus, # MemberDto.status +) +from devhelm._generated import Status9 as MemberStatus # MemberDto.status +from devhelm._generated import ( Status10 as NotificationDispatchStatus, # NotificationDispatchDto.status +) +from devhelm._generated import ( Status11 as PublishIncidentStatus, # PublishStatusPageIncidentRequest.status +) +from devhelm._generated import ( Status12 as ResourceGroupHealthStatus, # ResourceGroupHealthDto.status +) +from devhelm._generated import ( Status14 as CustomDomainStatus, # StatusPageCustomDomainDto.status +) +from devhelm._generated import ( Status15 as StatusPageUpdateStatus, # StatusPageIncidentUpdateDto.status - # - # Severity enums - Severity as AssertionSeverity, # AssertionResultDto.severity - Severity3 as IncidentSeverity, # CreateManualIncidentRequest.severity - Severity6 as MonitorAssertionSeverity, # MonitorAssertionDto.severity - Severity7 as TriggerRuleSeverity, # TriggerRule.severity - Severity8 as UpdateAssertionSeverity, # UpdateAssertionRequest.severity +) +from devhelm._generated import ( # # Type enums Type as ConfirmationPolicyType, # ConfirmationPolicy.type - Type1 as MonitorType, # CreateMonitorRequest.type +) +from devhelm._generated import Type1 as MonitorType # CreateMonitorRequest.type +from devhelm._generated import ( Type2 as StatusPageComponentType, # CreateStatusPageComponentRequest.type - Type3 as MonitorDtoType, # MonitorDto.type +) +from devhelm._generated import Type3 as MonitorDtoType # MonitorDto.type +from devhelm._generated import ( Type5 as StatusPageComponentDtoType, # StatusPageComponentDto.type - Type6 as TriggerRuleType, # TriggerRule.type - # - # CurrentStatus enums - CurrentStatus as MonitorCurrentStatus, # ResultSummaryDto.current_status - CurrentStatus1 as StatusPageComponentCurrentStatus, # StatusPageComponentDto.current_status - # - # CreatedBy enums - CreatedBy as IncidentUpdateCreatedBy, # IncidentUpdateDto.created_by - CreatedBy1 as StatusPageUpdateCreatedBy, # StatusPageIncidentUpdateDto.created_by - # - # NewStatus / OldStatus / OverallStatus / ComponentStatus — already - # semi-descriptive but aliased for consistency with the rest of the SDK. - NewStatus as IncidentNewStatus, # AddIncidentUpdateRequest.new_status - OldStatus as IncidentOldStatus, # IncidentUpdateDto.old_status - OverallStatus as StatusPageOverallStatus, # StatusPageDto.overall_status - ComponentStatus as StatusPageIncidentComponentStatus, # StatusPageIncidentComponentDto.component_status ) +from devhelm._generated import Type6 as TriggerRuleType # TriggerRule.type __all__ = [ # ── DTOs ────────────────────────────────────────────────────────────── "AcquireDeployLockRequest", "AddCustomDomainRequest", + "AddIncidentUpdateRequest", "AddResourceGroupMemberRequest", "AdminAddSubscriberRequest", "AlertChannelDto", diff --git a/tests/test_spec_parity.py b/tests/test_spec_parity.py new file mode 100644 index 0000000..8e9b67c --- /dev/null +++ b/tests/test_spec_parity.py @@ -0,0 +1,384 @@ +"""Spec parity tests. + +These tests are the canary for *logical* drift between the vendored +``docs/openapi/monitoring-api.json`` and the SDK surface. ``spec-check.yml`` +already verifies that the spec re-generates without crashing and that the +package compiles, but it does **not** notice when: + +* a public DTO that shipped in a previous SDK release silently disappears from + the spec (or gets renamed), or +* a required field on a shipped request shrinks or grows, or +* a path used by a hand-written resource method is no longer present in the + spec. + +The hand-built dictionaries in ``test_schemas.py`` exercise the *runtime* +behaviour of the Pydantic models, but they are decoupled from the spec — they +will keep passing even if the spec drops the schema entirely (because they +import the generated class, which is also re-generated from the same spec). +The assertions below close that loop by reading the OpenAPI document at test +time and asserting structural agreement against the SDK's public surface. +""" + +from __future__ import annotations + +import json +from collections.abc import Mapping +from pathlib import Path +from typing import Any, get_type_hints + +import pytest +from pydantic import BaseModel + +import devhelm +from devhelm import _generated +from devhelm.resources.alert_channels import AlertChannels +from devhelm.resources.api_keys import ApiKeys +from devhelm.resources.deploy_lock import DeployLock +from devhelm.resources.environments import Environments +from devhelm.resources.incidents import Incidents +from devhelm.resources.monitors import Monitors +from devhelm.resources.notification_policies import NotificationPolicies +from devhelm.resources.resource_groups import ResourceGroups +from devhelm.resources.secrets import Secrets +from devhelm.resources.status_pages import StatusPages +from devhelm.resources.status_pages import _Components as StatusPageComponents +from devhelm.resources.status_pages import _Domains as StatusPageCustomDomains +from devhelm.resources.status_pages import _Groups as StatusPageComponentGroups +from devhelm.resources.status_pages import _Incidents as StatusPageIncidents +from devhelm.resources.status_pages import _Subscribers as StatusPageSubscribers +from devhelm.resources.tags import Tags +from devhelm.resources.webhooks import Webhooks + +SPEC_PATH = Path(__file__).parent.parent / "docs" / "openapi" / "monitoring-api.json" + + +@pytest.fixture(scope="module") +def spec() -> dict[str, Any]: + """Load the vendored OpenAPI spec once per module.""" + return json.loads(SPEC_PATH.read_text()) + + +@pytest.fixture(scope="module") +def schemas(spec: dict[str, Any]) -> dict[str, Any]: + return spec["components"]["schemas"] + + +@pytest.fixture(scope="module") +def paths(spec: dict[str, Any]) -> dict[str, Any]: + return spec["paths"] + + +# ---------- 1. Schema parity: every public DTO must exist in the spec ---------- + + +# DTOs that are intentionally hand-defined in the SDK (not from the spec) or +# that the spec omits because they are response wrappers materialised +# server-side. Keep this list small and document each entry. +SCHEMAS_NOT_IN_SPEC: frozenset[str] = frozenset( + { + # Pagination wrappers — generated from `Page` / `CursorPage` generics + # and not present as standalone schemas in the OpenAPI document. + "Page", + "CursorPage", + } +) + + +def _public_dto_names() -> list[str]: + """Public DTOs the SDK re-exports that map 1:1 to spec schemas.""" + candidates: list[str] = [] + for name in devhelm.__all__: + obj = getattr(devhelm, name, None) + if obj is None: + continue + if not isinstance(obj, type): + continue + if not issubclass(obj, BaseModel): + continue + if name in SCHEMAS_NOT_IN_SPEC: + continue + candidates.append(name) + return sorted(candidates) + + +@pytest.mark.parametrize("dto_name", _public_dto_names()) +def test_public_dto_exists_in_spec(dto_name: str, schemas: dict[str, Any]) -> None: + """Every public DTO/Request type the SDK re-exports must exist in the spec.""" + assert dto_name in schemas, ( + f"{dto_name} is exported from `devhelm` but missing from the OpenAPI " + "spec. Either it was renamed/removed upstream (regenerate _generated.py) " + "or it should be added to SCHEMAS_NOT_IN_SPEC with a comment." + ) + + +# ---------- 2. Required-field parity for request DTOs ---------- + + +# ``RequestBody`` body parameters resolved per resource — every entry here is +# (model_class, ``method`` name) — kept narrow on purpose: these are the +# request shapes documented in the README and used in our typed examples. +REQUEST_DTO_NAMES: list[str] = sorted( + { + "AcquireDeployLockRequest", + "AddCustomDomainRequest", + "AddIncidentUpdateRequest", + "AddResourceGroupMemberRequest", + "AdminAddSubscriberRequest", + "CreateAlertChannelRequest", + "CreateApiKeyRequest", + "CreateEnvironmentRequest", + "CreateManualIncidentRequest", + "CreateMonitorRequest", + "CreateNotificationPolicyRequest", + "CreateResourceGroupRequest", + "CreateSecretRequest", + "CreateStatusPageComponentGroupRequest", + "CreateStatusPageComponentRequest", + "CreateStatusPageIncidentRequest", + "CreateStatusPageIncidentUpdateRequest", + "CreateStatusPageRequest", + "CreateTagRequest", + "CreateWebhookEndpointRequest", + "ReorderComponentsRequest", + "ResolveIncidentRequest", + "UpdateAlertChannelRequest", + "UpdateEnvironmentRequest", + "UpdateMonitorRequest", + "UpdateNotificationPolicyRequest", + "UpdateResourceGroupRequest", + "UpdateSecretRequest", + "UpdateStatusPageComponentGroupRequest", + "UpdateStatusPageComponentRequest", + "UpdateStatusPageIncidentRequest", + "UpdateStatusPageRequest", + "UpdateTagRequest", + "UpdateWebhookEndpointRequest", + } +) + + +@pytest.mark.parametrize("dto_name", REQUEST_DTO_NAMES) +def test_request_required_fields_match_spec( + dto_name: str, schemas: dict[str, Any] +) -> None: + """Required fields on the spec must be required on the Pydantic model. + + A request DTO going from required → optional in the spec without the + Pydantic model following suit means the SDK silently rejects payloads + the API would accept. The reverse — required field added to the spec + but missing on the model — means the SDK lets you send an obviously + malformed payload. Either way, this test catches the drift. + """ + spec_schema = schemas.get(dto_name) + assert spec_schema is not None, ( + f"{dto_name} listed in REQUEST_DTO_NAMES but missing from spec " + "(see test_public_dto_exists_in_spec for details)." + ) + + spec_required = set(spec_schema.get("required", [])) + + model_cls = getattr(_generated, dto_name) + model_required = { + # Pydantic stores the wire name as ``alias`` when one is set, and + # falls back to the Python attribute name otherwise. + (info.alias or field_name) + for field_name, info in model_cls.model_fields.items() + if info.is_required() + } + + # The spec is the source of truth; model_required must be a *superset* + # of spec_required. Models may legitimately mark additional fields as + # required for extra runtime safety, but they must never relax a field + # the spec considers required. + missing = spec_required - model_required + assert not missing, ( + f"{dto_name}: fields {sorted(missing)} are required in the OpenAPI " + "spec but optional on the generated Pydantic model. Re-run " + "`uv run datamodel-codegen` after pulling the latest spec, or align " + "the manual override." + ) + + +# ---------- 3. Method body parameters accept ``Mapping[str, Any]`` ---------- + + +# Resource classes whose mutating methods take ``RequestBody[T]`` body params. +# Listed here so a contributor adding a new resource gets the parity check +# for free. Each tuple is (resource_class, [(method_name, expected_model)]). +RESOURCE_BODY_METHODS: list[tuple[type, list[tuple[str, str]]]] = [ + ( + Monitors, + [("create", "CreateMonitorRequest"), ("update", "UpdateMonitorRequest")], + ), + ( + Incidents, + [ + ("create", "CreateManualIncidentRequest"), + ("resolve", "ResolveIncidentRequest"), + ], + ), + ( + AlertChannels, + [ + ("create", "CreateAlertChannelRequest"), + ("update", "UpdateAlertChannelRequest"), + ], + ), + ( + NotificationPolicies, + [ + ("create", "CreateNotificationPolicyRequest"), + ("update", "UpdateNotificationPolicyRequest"), + ], + ), + ( + Environments, + [ + ("create", "CreateEnvironmentRequest"), + ("update", "UpdateEnvironmentRequest"), + ], + ), + (Secrets, [("create", "CreateSecretRequest"), ("update", "UpdateSecretRequest")]), + (Tags, [("create", "CreateTagRequest"), ("update", "UpdateTagRequest")]), + ( + ResourceGroups, + [ + ("create", "CreateResourceGroupRequest"), + ("update", "UpdateResourceGroupRequest"), + ("add_member", "AddResourceGroupMemberRequest"), + ], + ), + ( + Webhooks, + [ + ("create", "CreateWebhookEndpointRequest"), + ("update", "UpdateWebhookEndpointRequest"), + ], + ), + (ApiKeys, [("create", "CreateApiKeyRequest")]), + (DeployLock, [("acquire", "AcquireDeployLockRequest")]), + ( + StatusPages, + [("create", "CreateStatusPageRequest"), ("update", "UpdateStatusPageRequest")], + ), + ( + StatusPageComponents, + [ + ("create", "CreateStatusPageComponentRequest"), + ("update", "UpdateStatusPageComponentRequest"), + ("reorder", "ReorderComponentsRequest"), + ], + ), + ( + StatusPageComponentGroups, + [ + ("create", "CreateStatusPageComponentGroupRequest"), + ("update", "UpdateStatusPageComponentGroupRequest"), + ], + ), + ( + StatusPageIncidents, + [ + ("create", "CreateStatusPageIncidentRequest"), + ("update", "UpdateStatusPageIncidentRequest"), + ("post_update", "CreateStatusPageIncidentUpdateRequest"), + ], + ), + (StatusPageSubscribers, [("add", "AdminAddSubscriberRequest")]), + (StatusPageCustomDomains, [("add", "AddCustomDomainRequest")]), +] + + +def _resource_method_params() -> list[tuple[type, str, str]]: + return [ + (cls, method, expected) + for cls, methods in RESOURCE_BODY_METHODS + for method, expected in methods + ] + + +@pytest.mark.parametrize( + "resource_cls,method_name,expected_model", + _resource_method_params(), + ids=lambda v: v if isinstance(v, str) else v.__name__, +) +def test_resource_method_body_accepts_dict( + resource_cls: type, method_name: str, expected_model: str +) -> None: + """Every mutating resource method's ``body`` param accepts a dict. + + The ticket calls out the dict-vs-model ergonomics conflict: README + examples show dicts (``client.monitors.create({"name": "foo"})``) but + earlier signatures only accepted the Pydantic model. We resolved this + by widening the type to ``RequestBody[T] = T | Mapping[str, Any]``. + The assertion below ensures every body parameter actually has the + union — without it, ``mypy --strict`` would reject the documented + usage. + """ + method = getattr(resource_cls, method_name) + hints = get_type_hints(method) + assert "body" in hints, f"{resource_cls.__name__}.{method_name} has no body param" + annotation = hints["body"] + + # ``RequestBody[T]`` resolves to a ``Union[T, Mapping[str, Any]]``. + # ``get_type_hints`` returns it as ``typing.Union`` so we can introspect + # the args directly. Optional bodies (``RequestBody[T] | None``) carry + # an extra ``NoneType`` arm. + args = getattr(annotation, "__args__", ()) + assert args, ( + f"{resource_cls.__name__}.{method_name} body annotation is not a Union; " + f"got {annotation!r}" + ) + + arg_names = {getattr(a, "__name__", str(a)) for a in args} + # Mapping[str, Any] becomes typing.Mapping after ``get_type_hints``; its + # origin is collections.abc.Mapping. We accept either spelling. + has_mapping = any(getattr(a, "__origin__", None) is Mapping for a in args) + assert has_mapping, ( + f"{resource_cls.__name__}.{method_name} body should accept " + f"Mapping[str, Any] for dict ergonomics; got args {arg_names}" + ) + assert expected_model in arg_names, ( + f"{resource_cls.__name__}.{method_name} body should also accept " + f"the typed {expected_model}; got args {arg_names}" + ) + + +# ---------- 4. Hand-written paths exist in the spec ---------- + + +# Paths the SDK calls that aren't bound to a single resource method we want +# to enumerate above. These are the substring matchers we use to assert the +# path appears in the spec at all (the SDK templates ``{id}`` etc. by hand, +# so we check the leading segments). +SDK_PATH_PREFIXES: list[str] = [ + "/api/v1/monitors", + "/api/v1/incidents", + "/api/v1/alert-channels", + "/api/v1/notification-policies", + "/api/v1/environments", + "/api/v1/secrets", + "/api/v1/tags", + "/api/v1/resource-groups", + "/api/v1/webhooks", + "/api/v1/api-keys", + "/api/v1/deploy/lock", + "/api/v1/status-pages", + "/api/v1/service-subscriptions", +] + + +@pytest.mark.parametrize("prefix", SDK_PATH_PREFIXES) +def test_sdk_path_prefix_in_spec(prefix: str, paths: dict[str, Any]) -> None: + """Every top-level path the SDK hits must exist in the spec. + + Catches the case where the API renames an endpoint family (e.g. + ``/v1/monitors`` → ``/v2/monitors``) and the SDK still calls the old + one. ``spec-check.yml`` would not catch this because the spec still + parses cleanly. + """ + assert any(p.startswith(prefix) for p in paths), ( + f"No spec path starts with {prefix}. Either the API was renamed " + "(update the SDK resources) or this prefix should be removed from " + "SDK_PATH_PREFIXES." + ) From c12366a6f720c3a52ef6fd918c0bee57c8a81e05 Mon Sep 17 00:00:00 2001 From: caballeto Date: Mon, 20 Apr 2026 13:54:44 +0200 Subject: [PATCH 06/10] chore: prune dead code, tighten typing, add (method, path) parity test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - END-1082 Remove dead unwrap_single helper and stop generating the unused CursorPage model (the API now returns concrete cursor pages per resource so the generic was unreachable). Regenerate _generated.py from the refreshed vendored spec. - _http.py / _pagination.py: tighten typing — explicit dict[str, Any] annotations, drop the implicit Any return paths flagged by mypy. - Mirror the SDK-JS spec-paths test in tests/test_spec_parity.py with a (method, path) check for every resource the SDK calls. Same fix applied: incidents.delete() (non-existent endpoint) is removed; the rest of the table now lines up with the OpenAPI spec. - Refresh test_negative_validation.py / test_schemas.py to reflect the optional/nullable fields the API actually emits (ResolveIncidentRequest, CreateNotificationPolicyRequest, etc.) so previously stale assertions pass again. Add tests/test_typing.py for shared helpers. - Refresh docs/openapi/monitoring-api.json to match the new mini spec (incl. /api/v1/alert-channels/{id} GET endpoint). ruff format --check, ruff check, mypy --strict (src + tests), pytest (681 tests) all green. Made-with: Cursor --- docs/openapi/monitoring-api.json | 22954 +++++++++++++++++++++++----- src/devhelm/_generated.py | 457 +- src/devhelm/_http.py | 4 +- src/devhelm/_pagination.py | 12 +- tests/test_http.py | 1 - tests/test_negative_validation.py | 166 +- tests/test_schemas.py | 6 +- tests/test_spec_parity.py | 9 +- tests/test_typing.py | 66 + 9 files changed, 19354 insertions(+), 4321 deletions(-) create mode 100644 tests/test_typing.py diff --git a/docs/openapi/monitoring-api.json b/docs/openapi/monitoring-api.json index d3dfb3f..11940cb 100644 --- a/docs/openapi/monitoring-api.json +++ b/docs/openapi/monitoring-api.json @@ -183,6 +183,86 @@ } } } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } }, @@ -212,17 +292,97 @@ } } } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } }, "/api/v1/alert-channels/{id}": { - "put": { + "get": { "tags": [ "Alert Channels" ], - "summary": "Update an alert channel's name and re-encrypt config", - "operationId": "update_14", + "summary": "Get a single alert channel by id", + "operationId": "get_8", "parameters": [ { "name": "id", @@ -234,16 +394,6 @@ } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateAlertChannelRequest" - } - } - }, - "required": true - }, "responses": { "200": { "description": "OK", @@ -254,15 +404,95 @@ } } } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } }, - "delete": { + "put": { "tags": [ "Alert Channels" ], - "summary": "Soft-delete an alert channel and return affected policy summary", - "operationId": "delete_10", + "summary": "Update an alert channel's name and re-encrypt config", + "operationId": "update_14", "parameters": [ { "name": "id", @@ -274,27 +504,115 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAlertChannelRequest" + } + } + }, + "required": true + }, "responses": { "200": { "description": "OK", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/DeleteChannelResult" + "$ref": "#/components/schemas/SingleValueResponseAlertChannelDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } } } - } - }, - "/api/v1/alert-channels/{id}/deliveries": { - "get": { + }, + "delete": { "tags": [ "Alert Channels" ], - "summary": "List delivery history for an alert channel", - "operationId": "listDeliveries_1", + "summary": "Soft-delete an alert channel and return affected policy summary", + "operationId": "delete_10", "parameters": [ { "name": "id", @@ -312,70 +630,87 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/TableValueResultAlertDeliveryDto" + "$ref": "#/components/schemas/DeleteChannelResult" } } } - } - } - } - }, - "/api/v1/alert-channels/{id}/test": { - "post": { - "tags": [ - "Alert Channels" - ], - "summary": "Test a saved alert channel's connectivity", - "operationId": "test_2", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseTestChannelResult" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/alert-channels/test": { - "post": { - "tags": [ - "Alert Channels" - ], - "summary": "Test alert channel connectivity using raw config (no saved channel required)", - "operationId": "testConfig", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TestAlertChannelRequest" + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "404": { + "description": "Not found — the requested resource does not exist", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseTestChannelResult" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -383,14 +718,13 @@ } } }, - "/api/v1/alert-deliveries/{id}/attempts": { + "/api/v1/alert-channels/{id}/deliveries": { "get": { "tags": [ - "Alert Deliveries" + "Alert Channels" ], - "summary": "List delivery attempts for a specific alert delivery", - "description": "Returns the ordered list of delivery attempts (request/response audit data) for the given delivery ID.", - "operationId": "listAttempts", + "summary": "List delivery history for an alert channel", + "operationId": "listDeliveries_1", "parameters": [ { "name": "id", @@ -408,7 +742,87 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/TableValueResultDeliveryAttemptDto" + "$ref": "#/components/schemas/TableValueResultAlertDeliveryDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -416,14 +830,13 @@ } } }, - "/api/v1/alert-deliveries/{id}/retry": { + "/api/v1/alert-channels/{id}/test": { "post": { "tags": [ - "Alert Deliveries" + "Alert Channels" ], - "summary": "Retry a failed delivery", - "description": "Resets a FAILED delivery to RETRY_PENDING so the delivery worker re-attempts it.", - "operationId": "retry", + "summary": "Test a saved alert channel's connectivity", + "operationId": "test_2", "parameters": [ { "name": "id", @@ -441,122 +854,198 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseAlertDeliveryDto" + "$ref": "#/components/schemas/SingleValueResponseTestChannelResult" } } } - } - } - } - }, - "/api/v1/api-keys": { - "get": { - "tags": [ - "API Keys" - ], - "summary": "List API keys", - "operationId": "list_13", - "responses": { - "200": { - "description": "OK", + }, + "400": { + "description": "Bad request — the payload failed validation", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultApiKeyDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } } } - }, + } + }, + "/api/v1/alert-channels/test": { "post": { "tags": [ - "API Keys" + "Alert Channels" ], - "summary": "Create API key", - "operationId": "create_14", + "summary": "Test alert channel connectivity using raw config (no saved channel required)", + "operationId": "testConfig", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateApiKeyRequest" + "$ref": "#/components/schemas/TestAlertChannelRequest" } } }, "required": true }, "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseApiKeyCreateResponse" + "$ref": "#/components/schemas/SingleValueResponseTestChannelResult" } } } - } - } - } - }, - "/api/v1/api-keys/{id}": { - "delete": { - "tags": [ - "API Keys" - ], - "summary": "Delete API key", - "operationId": "delete_11", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - }, - "patch": { - "tags": [ - "API Keys" - ], - "summary": "Update API key", - "operationId": "update_15", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateApiKeyRequest" + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "404": { + "description": "Not found — the requested resource does not exist", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseApiKeyDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -564,21 +1053,22 @@ } } }, - "/api/v1/api-keys/{id}/regenerate": { - "post": { + "/api/v1/alert-deliveries/{id}/attempts": { + "get": { "tags": [ - "API Keys" + "Alert Deliveries" ], - "summary": "Regenerate API key", - "operationId": "regenerate", + "summary": "List delivery attempts for a specific alert delivery", + "description": "Returns the ordered list of delivery attempts (request/response audit data) for the given delivery ID.", + "operationId": "listAttempts", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string", + "format": "uuid" } } ], @@ -588,7 +1078,87 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseApiKeyCreateResponse" + "$ref": "#/components/schemas/TableValueResultDeliveryAttemptDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -596,21 +1166,22 @@ } } }, - "/api/v1/api-keys/{id}/revoke": { + "/api/v1/alert-deliveries/{id}/retry": { "post": { "tags": [ - "API Keys" + "Alert Deliveries" ], - "summary": "Revoke API key", - "operationId": "revoke_1", + "summary": "Retry a failed delivery", + "description": "Resets a FAILED delivery to RETRY_PENDING so the delivery worker re-attempts it.", + "operationId": "retry", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string", + "format": "uuid" } } ], @@ -620,93 +1191,87 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseApiKeyDto" + "$ref": "#/components/schemas/SingleValueResponseAlertDeliveryDto" } } } - } - } - } - }, - "/api/v1/audit-log": { - "get": { - "tags": [ - "Audit Log" - ], - "summary": "List audit events for the current organization", - "operationId": "list_19", - "parameters": [ - { - "name": "action", - "in": "query", - "required": false, - "schema": { - "type": "string" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "actorId", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int32" + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "resourceType", - "in": "query", - "required": false, - "schema": { - "type": "string" + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "from", - "in": "query", - "required": false, - "schema": { - "type": "string", - "format": "date-time" + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "to", - "in": "query", - "required": false, - "schema": { - "type": "string", - "format": "date-time" + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "page", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 0 + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "size", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 50 + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "503": { + "description": "Service unavailable — try again shortly", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultAuditEventDto" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -714,178 +1279,100 @@ } } }, - "/api/v1/auth/me": { + "/api/v1/api-keys": { "get": { "tags": [ - "API Auth" + "API Keys" ], - "summary": "Get current API key identity", - "description": "Returns the authenticated API key's metadata, organization, billing plan, entitlements with usage, and current rate-limit quota. Only available for API key authentication (Bearer dh_live_...).", - "operationId": "me_1", + "summary": "List API keys", + "operationId": "list_13", "responses": { "200": { "description": "OK", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseAuthMeResponse" + "$ref": "#/components/schemas/TableValueResultApiKeyDto" } } } - } - } - } - }, - "/api/v1/categories": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "List categories with service counts", - "operationId": "listCategories", - "responses": { - "200": { - "description": "OK", + }, + "400": { + "description": "Bad request — the payload failed validation", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultCategoryDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/dashboard/overview": { - "get": { - "tags": [ - "Dashboard" - ], - "summary": "Dashboard overview", - "description": "Returns monitor status counts, average uptime windows, and incident aggregates for the authenticated org. Results are cached for 1 minute.", - "operationId": "overview", - "responses": { - "200": { - "description": "OK", + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseDashboardOverviewDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/deploy/lock": { - "get": { - "tags": [ - "Deploy Lock" - ], - "summary": "Get current deploy lock", - "description": "Returns the active deploy lock for the current workspace, if any.", - "operationId": "current", - "responses": { - "200": { - "description": "OK", + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseDeployLockDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "post": { - "tags": [ - "Deploy Lock" - ], - "summary": "Acquire deploy lock", - "description": "Acquires an exclusive deploy lock for the current workspace. Returns 409 Conflict if the workspace is already locked by another session.", - "operationId": "acquire", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AcquireDeployLockRequest" + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "409": { + "description": "Conflict — the request collides with current resource state", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseDeployLockDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/deploy/lock/{lockId}": { - "delete": { - "tags": [ - "Deploy Lock" - ], - "summary": "Release deploy lock", - "description": "Releases a deploy lock by ID. Only the lock holder should call this.", - "operationId": "release", - "parameters": [ - { - "name": "lockId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/deploy/lock/force": { - "delete": { - "tags": [ - "Deploy Lock" - ], - "summary": "Force-release deploy lock", - "description": "Forcibly removes any deploy lock on the current workspace. Use to break stale locks.", - "operationId": "forceRelease", - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/environments": { - "get": { - "tags": [ - "Environments" - ], - "summary": "List environments", - "operationId": "list_12", - "responses": { - "200": { - "description": "OK", + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultEnvironmentDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -894,15 +1381,15 @@ }, "post": { "tags": [ - "Environments" + "API Keys" ], - "summary": "Create environment", - "operationId": "create_13", + "summary": "Create API key", + "operationId": "create_14", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateEnvironmentRequest" + "$ref": "#/components/schemas/CreateApiKeyRequest" } } }, @@ -914,7 +1401,87 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseEnvironmentDto" + "$ref": "#/components/schemas/SingleValueResponseApiKeyCreateResponse" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -922,148 +1489,124 @@ } } }, - "/api/v1/environments/{slug}": { - "get": { + "/api/v1/api-keys/{id}": { + "delete": { "tags": [ - "Environments" + "API Keys" ], - "summary": "Get environment by slug", - "operationId": "get_7", + "summary": "Delete API key", + "operationId": "delete_11", "parameters": [ { - "name": "slug", + "name": "id", "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int32" } } ], "responses": { - "200": { - "description": "OK", + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseEnvironmentDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "put": { - "tags": [ - "Environments" - ], - "summary": "Update environment", - "operationId": "update_13", - "parameters": [ - { - "name": "slug", - "in": "path", - "required": true, - "schema": { - "type": "string" + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateEnvironmentRequest" + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "404": { + "description": "Not found — the requested resource does not exist", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseEnvironmentDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "delete": { - "tags": [ - "Environments" - ], - "summary": "Delete environment", - "operationId": "delete_9", - "parameters": [ - { - "name": "slug", - "in": "path", - "required": true, - "schema": { - "type": "string" + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/heartbeat/{token}": { - "get": { - "tags": [ - "Heartbeat" - ], - "summary": "Record a heartbeat ping (GET)", - "description": "Called by external systems (cron jobs, scheduled tasks) to signal liveness. Always returns 200 OK.", - "operationId": "pingGet", - "parameters": [ - { - "name": "token", - "in": "path", - "description": "Ping endpoint token for the heartbeat monitor", - "required": true, - "schema": { - "type": "string" + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", "content": { - "*/*": { + "application/json": { "schema": { - "type": "object", - "additionalProperties": { - "type": "boolean" - } + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } } } }, - "post": { + "patch": { "tags": [ - "Heartbeat" + "API Keys" ], - "summary": "Record a heartbeat ping (POST)", - "description": "Called by external systems to signal liveness with an optional JSON payload. The payload can be inspected by heartbeat_payload_contains assertions. Always returns 200 OK.", - "operationId": "pingPost", + "summary": "Update API key", + "operationId": "update_15", "parameters": [ { - "name": "token", + "name": "id", "in": "path", - "description": "Ping endpoint token for the heartbeat monitor", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int32" } } ], @@ -1071,20 +1614,11 @@ "content": { "application/json": { "schema": { - "type": "string" - } - }, - "text/plain": { - "schema": { - "type": "string" - } - }, - "*/*": { - "schema": { - "type": "string" + "$ref": "#/components/schemas/UpdateApiKeyRequest" } } - } + }, + "required": true }, "responses": { "200": { @@ -1092,102 +1626,87 @@ "content": { "*/*": { "schema": { - "type": "object", - "additionalProperties": { - "type": "boolean" - } + "$ref": "#/components/schemas/SingleValueResponseApiKeyDto" } } } - } - } - } - }, - "/api/v1/incidents": { - "get": { - "tags": [ - "Incidents" - ], - "summary": "List incidents for the authenticated org", - "operationId": "list_11", - "parameters": [ - { - "name": "params", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/IncidentFilterParams" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultIncidentDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "post": { - "tags": [ - "Incidents" - ], - "summary": "Create a manual incident", - "operationId": "create_12", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateManualIncidentRequest" + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "404": { + "description": "Not found — the requested resource does not exist", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentDetailDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/incidents/{id}": { - "get": { - "tags": [ - "Incidents" - ], - "summary": "Get incident details including update timeline", - "operationId": "get_10", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "500": { + "description": "Internal server error — see the message field for details", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentDetailDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -1195,154 +1714,111 @@ } } }, - "/api/v1/incidents/{id}/resolve": { + "/api/v1/api-keys/{id}/regenerate": { "post": { "tags": [ - "Incidents" + "API Keys" ], - "summary": "Resolve an incident", - "operationId": "resolve", + "summary": "Regenerate API key", + "operationId": "regenerate", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "integer", + "format": "int32" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResolveIncidentRequest" - } - } - } - }, "responses": { "200": { "description": "OK", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentDetailDto" + "$ref": "#/components/schemas/SingleValueResponseApiKeyCreateResponse" } } } - } - } - } - }, - "/api/v1/incidents/{id}/updates": { - "post": { - "tags": [ - "Incidents" - ], - "summary": "Add an update to an incident (optionally change status)", - "operationId": "addUpdate", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AddIncidentUpdateRequest" + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "403": { + "description": "Forbidden — the actor lacks permission for this resource", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentDetailDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/integrations": { - "get": { - "tags": [ - "Integrations" - ], - "summary": "List all supported integration types", - "description": "Returns the full static catalog of supported alert channel integration types with their metadata and config field schemas. Used by the frontend to dynamically render the 'Add Alert Channel' form.", - "operationId": "list_18", - "responses": { - "200": { - "description": "OK", + }, + "404": { + "description": "Not found — the requested resource does not exist", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultIntegrationDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/invites": { - "get": { - "tags": [ - "Invites" - ], - "summary": "List invites", - "operationId": "list_10", - "responses": { - "200": { - "description": "OK", + }, + "409": { + "description": "Conflict — the request collides with current resource state", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultInviteDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "post": { - "tags": [ - "Invites" - ], - "summary": "Create invite", - "operationId": "create_11", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateInviteRequest" + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "502": { + "description": "Bad gateway — an upstream provider returned an error", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseInviteDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -1350,16 +1826,16 @@ } } }, - "/api/v1/invites/{inviteId}/resend": { + "/api/v1/api-keys/{id}/revoke": { "post": { "tags": [ - "Invites" + "API Keys" ], - "summary": "Resend invite", - "operationId": "resend", + "summary": "Revoke API key", + "operationId": "revoke_1", "parameters": [ { - "name": "inviteId", + "name": "id", "in": "path", "required": true, "schema": { @@ -1374,7 +1850,87 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseInviteDto" + "$ref": "#/components/schemas/SingleValueResponseApiKeyDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -1382,57 +1938,75 @@ } } }, - "/api/v1/invites/{inviteId}/revoke": { - "post": { + "/api/v1/audit-log": { + "get": { "tags": [ - "Invites" + "Audit Log" ], - "summary": "Revoke invite", - "operationId": "revoke", + "summary": "List audit events for the current organization", + "operationId": "list_19", "parameters": [ { - "name": "inviteId", - "in": "path", - "required": true, + "name": "action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "actorId", + "in": "query", + "required": false, "schema": { "type": "integer", "format": "int32" } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/maintenance-windows": { - "get": { - "tags": [ - "Maintenance Windows" - ], - "summary": "List maintenance windows for the authenticated org", - "description": "Returns maintenance windows for the caller's organisation. Optionally filter by monitor_id, and/or by status: 'active' (currently in window) or 'upcoming' (starts in the future).", - "operationId": "list_9", - "parameters": [ + }, { - "name": "monitorId", + "name": "resourceType", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "from", "in": "query", - "description": "Filter by monitor UUID", "required": false, "schema": { "type": "string", - "format": "uuid" + "format": "date-time" } }, { - "name": "filter", + "name": "to", "in": "query", - "description": "Filter by status: 'active' or 'upcoming'", "required": false, "schema": { - "type": "string" + "type": "string", + "format": "date-time" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 50 } } ], @@ -1442,37 +2016,87 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/TableValueResultMaintenanceWindowDto" + "$ref": "#/components/schemas/TableValueResultAuditEventDto" } } } - } - } - }, - "post": { - "tags": [ - "Maintenance Windows" - ], - "summary": "Create a maintenance window", - "description": "Creates a new maintenance window. Set monitorId to null to create an org-wide window that suppresses alerts for all monitors.", - "operationId": "create_10", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateMaintenanceWindowRequest" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "401": { + "description": "Unauthorized — missing or invalid credentials", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseMaintenanceWindowDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -1480,125 +2104,202 @@ } } }, - "/api/v1/maintenance-windows/{id}": { + "/api/v1/auth/me": { "get": { "tags": [ - "Maintenance Windows" - ], - "summary": "Get a single maintenance window by ID", - "operationId": "getById_2", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } + "API Auth" ], + "summary": "Get current API key identity", + "description": "Returns the authenticated API key's metadata, organization, billing plan, entitlements with usage, and current rate-limit quota. Only available for API key authentication (Bearer dh_live_...).", + "operationId": "me_1", "responses": { "200": { "description": "OK", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseMaintenanceWindowDto" + "$ref": "#/components/schemas/SingleValueResponseAuthMeResponse" } } } - } - } - }, - "put": { - "tags": [ - "Maintenance Windows" - ], - "summary": "Update a maintenance window", - "operationId": "update_12", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateMaintenanceWindowRequest" + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "403": { + "description": "Forbidden — the actor lacks permission for this resource", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseMaintenanceWindowDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "delete": { - "tags": [ - "Maintenance Windows" - ], - "summary": "Delete a maintenance window", - "operationId": "delete_8", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "204": { - "description": "No Content" } } } }, - "/api/v1/members": { + "/api/v1/categories": { "get": { "tags": [ - "Members" - ], - "summary": "List organization members", - "operationId": "list_17", - "parameters": [ - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" - } - } + "Status Data" ], + "summary": "List categories with service counts", + "operationId": "listCategories", "responses": { "200": { "description": "OK", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/TableValueResultMemberDto" + "$ref": "#/components/schemas/TableValueResultCategoryDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -1606,81 +2307,13079 @@ } } }, - "/api/v1/members/{userId}": { - "delete": { + "/api/v1/dashboard/overview": { + "get": { + "tags": [ + "Dashboard" + ], + "summary": "Dashboard overview", + "description": "Returns monitor status counts, average uptime windows, and incident aggregates for the authenticated org. Results are cached for 1 minute.", + "operationId": "overview", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseDashboardOverviewDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/deploy/lock": { + "get": { + "tags": [ + "Deploy Lock" + ], + "summary": "Get current deploy lock", + "description": "Returns the active deploy lock for the current workspace, if any.", + "operationId": "current", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseDeployLockDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "Deploy Lock" + ], + "summary": "Acquire deploy lock", + "description": "Acquires an exclusive deploy lock for the current workspace. Returns 409 Conflict if the workspace is already locked by another session.", + "operationId": "acquire", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AcquireDeployLockRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseDeployLockDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/deploy/lock/{lockId}": { + "delete": { + "tags": [ + "Deploy Lock" + ], + "summary": "Release deploy lock", + "description": "Releases a deploy lock by ID. Only the lock holder should call this.", + "operationId": "release", + "parameters": [ + { + "name": "lockId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/deploy/lock/force": { + "delete": { + "tags": [ + "Deploy Lock" + ], + "summary": "Force-release deploy lock", + "description": "Forcibly removes any deploy lock on the current workspace. Use to break stale locks.", + "operationId": "forceRelease", + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/environments": { + "get": { + "tags": [ + "Environments" + ], + "summary": "List environments", + "operationId": "list_12", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultEnvironmentDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "Environments" + ], + "summary": "Create environment", + "operationId": "create_13", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEnvironmentRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseEnvironmentDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/environments/{slug}": { + "get": { + "tags": [ + "Environments" + ], + "summary": "Get environment by slug", + "operationId": "get_7", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseEnvironmentDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "put": { + "tags": [ + "Environments" + ], + "summary": "Update environment", + "operationId": "update_13", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEnvironmentRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseEnvironmentDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Environments" + ], + "summary": "Delete environment", + "operationId": "delete_9", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/heartbeat/{token}": { + "get": { + "tags": [ + "Heartbeat" + ], + "summary": "Record a heartbeat ping (GET)", + "description": "Called by external systems (cron jobs, scheduled tasks) to signal liveness. Always returns 200 OK.", + "operationId": "pingGet", + "parameters": [ + { + "name": "token", + "in": "path", + "description": "Ping endpoint token for the heartbeat monitor", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/HeartbeatPingResponse" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "Heartbeat" + ], + "summary": "Record a heartbeat ping (POST)", + "description": "Called by external systems to signal liveness with an optional JSON payload. The payload can be inspected by heartbeat_payload_contains assertions. Always returns 200 OK.", + "operationId": "pingPost", + "parameters": [ + { + "name": "token", + "in": "path", + "description": "Ping endpoint token for the heartbeat monitor", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "*/*": { + "schema": { + "type": "string" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/HeartbeatPingResponse" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/incidents": { + "get": { + "tags": [ + "Incidents" + ], + "summary": "List incidents for the authenticated org", + "operationId": "list_11", + "parameters": [ + { + "name": "params", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/IncidentFilterParams" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultIncidentDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "Incidents" + ], + "summary": "Create a manual incident", + "operationId": "create_12", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateManualIncidentRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentDetailDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/incidents/{id}": { + "get": { + "tags": [ + "Incidents" + ], + "summary": "Get incident details including update timeline", + "operationId": "get_11", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentDetailDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/incidents/{id}/resolve": { + "post": { + "tags": [ + "Incidents" + ], + "summary": "Resolve an incident", + "operationId": "resolve", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResolveIncidentRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentDetailDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/incidents/{id}/updates": { + "post": { + "tags": [ + "Incidents" + ], + "summary": "Add an update to an incident (optionally change status)", + "operationId": "addUpdate", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddIncidentUpdateRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentDetailDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/integrations": { + "get": { + "tags": [ + "Integrations" + ], + "summary": "List all supported integration types", + "description": "Returns the full static catalog of supported alert channel integration types with their metadata and config field schemas. Used by the frontend to dynamically render the 'Add Alert Channel' form.", + "operationId": "list_18", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultIntegrationDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/invites": { + "get": { + "tags": [ + "Invites" + ], + "summary": "List invites", + "operationId": "list_10", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultInviteDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "Invites" + ], + "summary": "Create invite", + "operationId": "create_11", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateInviteRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseInviteDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/invites/{inviteId}/resend": { + "post": { + "tags": [ + "Invites" + ], + "summary": "Resend invite", + "operationId": "resend", + "parameters": [ + { + "name": "inviteId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseInviteDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/invites/{inviteId}/revoke": { + "post": { + "tags": [ + "Invites" + ], + "summary": "Revoke invite", + "operationId": "revoke", + "parameters": [ + { + "name": "inviteId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/maintenance-windows": { + "get": { + "tags": [ + "Maintenance Windows" + ], + "summary": "List maintenance windows for the authenticated org", + "description": "Returns maintenance windows for the caller's organisation. Optionally filter by monitor_id, and/or by status: 'active' (currently in window) or 'upcoming' (starts in the future).", + "operationId": "list_9", + "parameters": [ + { + "name": "monitorId", + "in": "query", + "description": "Filter by monitor UUID", + "required": false, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "filter", + "in": "query", + "description": "Filter by status: 'active' or 'upcoming'", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultMaintenanceWindowDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "Maintenance Windows" + ], + "summary": "Create a maintenance window", + "description": "Creates a new maintenance window. Set monitorId to null to create an org-wide window that suppresses alerts for all monitors.", + "operationId": "create_10", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMaintenanceWindowRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMaintenanceWindowDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/maintenance-windows/{id}": { + "get": { + "tags": [ + "Maintenance Windows" + ], + "summary": "Get a single maintenance window by ID", + "operationId": "getById_2", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMaintenanceWindowDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "put": { + "tags": [ + "Maintenance Windows" + ], + "summary": "Update a maintenance window", + "operationId": "update_12", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMaintenanceWindowRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMaintenanceWindowDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Maintenance Windows" + ], + "summary": "Delete a maintenance window", + "operationId": "delete_8", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/members": { + "get": { + "tags": [ + "Members" + ], + "summary": "List organization members", + "operationId": "list_17", + "parameters": [ + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultMemberDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/members/{userId}": { + "delete": { + "tags": [ + "Members" + ], + "summary": "Remove member from organization", + "operationId": "remove_2", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/members/{userId}/role": { + "put": { + "tags": [ + "Members" + ], + "summary": "Change member role", + "operationId": "changeRole", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeRoleRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/members/{userId}/status": { + "put": { + "tags": [ + "Members" + ], + "summary": "Change member status", + "operationId": "changeStatus", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeStatusRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors": { + "get": { + "tags": [ + "Monitors" + ], + "summary": "List monitors for the authenticated org", + "operationId": "list_8", + "parameters": [ + { + "name": "enabled", + "in": "query", + "description": "Filter by enabled state", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "type", + "in": "query", + "description": "Filter by monitor type", + "required": false, + "schema": { + "type": "string", + "enum": [ + "HTTP", + "DNS", + "MCP_SERVER", + "TCP", + "ICMP", + "HEARTBEAT" + ] + } + }, + { + "name": "managedBy", + "in": "query", + "description": "Filter by managed-by source", + "required": false, + "schema": { + "type": "string", + "enum": [ + "DASHBOARD", + "CLI", + "TERRAFORM" + ] + } + }, + { + "name": "tags", + "in": "query", + "description": "Filter by tag names, comma-separated (e.g. prod,critical)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "search", + "in": "query", + "description": "Case-insensitive name search", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "environmentId", + "in": "query", + "description": "Filter by environment ID", + "required": false, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultMonitorDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "Monitors" + ], + "summary": "Create a new monitor", + "operationId": "create_9", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMonitorRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}": { + "get": { + "tags": [ + "Monitors" + ], + "summary": "Get a single monitor by id", + "operationId": "get_6", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "put": { + "tags": [ + "Monitors" + ], + "summary": "Update a monitor", + "operationId": "update_11", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMonitorRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Monitors" + ], + "summary": "Soft-delete a monitor", + "operationId": "delete_7", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/pause": { + "post": { + "tags": [ + "Monitors" + ], + "summary": "Pause a monitor (set enabled=false)", + "operationId": "pause", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/results": { + "get": { + "tags": [ + "Check Results" + ], + "summary": "List raw check results", + "description": "Returns check results for the given monitor with optional time-range, region, and pass/fail filtering. Uses cursor-based pagination — pass the returned `cursor` value on subsequent requests to retrieve the next page. The cursor encodes the original time bounds, so `from`/`to` are ignored when a cursor is present.", + "operationId": "getResults", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "from", + "in": "query", + "description": "Start of time range (ISO 8601, inclusive); defaults to 24 hours ago", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "to", + "in": "query", + "description": "End of time range (ISO 8601, inclusive); defaults to now", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "cursor", + "in": "query", + "description": "Opaque cursor from a previous response for pagination", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum results per page (1–200)", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 50 + }, + "example": 50 + }, + { + "name": "region", + "in": "query", + "description": "Filter by region (e.g. us-east)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "passed", + "in": "query", + "description": "Filter by pass/fail status", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Paginated check results", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CursorPageCheckResultDto" + } + } + } + }, + "400": { + "description": "Invalid query parameters", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CursorPageCheckResultDto" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Monitor does not belong to the caller's org", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CursorPageCheckResultDto" + } + } + } + }, + "404": { + "description": "Monitor not found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CursorPageCheckResultDto" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/results/summary": { + "get": { + "tags": [ + "Check Results" + ], + "summary": "Get results summary", + "description": "Returns a dashboard summary for the monitor: current status derived from the latest result per region, time-bucketed chart data, the 24-hour uptime percentage, and the selected window's uptime percentage.", + "operationId": "getSummary", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "chartWindow", + "in": "query", + "description": "Chart window: 24h returns hourly buckets, 7d/30d/90d return daily buckets", + "required": false, + "schema": { + "type": "string", + "enum": [ + "24h", + "7d", + "30d", + "90d" + ] + } + } + ], + "responses": { + "200": { + "description": "Results summary", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ResultSummaryDto" + } + } + } + }, + "400": { + "description": "Invalid chartWindow parameter", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseResultSummaryDto" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Monitor does not belong to the caller's org", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseResultSummaryDto" + } + } + } + }, + "404": { + "description": "Monitor not found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseResultSummaryDto" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/resume": { + "post": { + "tags": [ + "Monitors" + ], + "summary": "Resume a monitor (set enabled=true)", + "operationId": "resume", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/rotate-token": { + "post": { + "tags": [ + "Monitors" + ], + "summary": "Rotate the ping token for a heartbeat monitor", + "description": "Generates a new ping token. The old token remains valid for 24 hours to allow cron jobs to be updated without downtime. Only supported for HEARTBEAT monitors.", + "operationId": "rotateToken", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/tags": { + "get": { + "tags": [ + "Monitors" + ], + "summary": "Get all tags applied to a monitor", + "operationId": "getMonitorTags", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultTagDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "Monitors" + ], + "summary": "Add tags to a monitor; supports existing tag IDs and inline creation of new tags", + "operationId": "addMonitorTags", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddMonitorTagsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultTagDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Monitors" + ], + "summary": "Remove tags from a monitor by their IDs", + "operationId": "removeMonitorTags", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoveMonitorTagsRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/test": { + "post": { + "tags": [ + "Monitors" + ], + "summary": "Test an existing monitor", + "description": "Runs the saved config and assertions of an existing monitor once, without persisting any result. Runs synchronously and returns the same shape as the ad-hoc test.", + "operationId": "testExisting", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorTestResultDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/uptime": { + "get": { + "tags": [ + "Check Results" + ], + "summary": "Get uptime statistics", + "description": "Returns uptime percentage and latency statistics for the requested time window, computed from continuous aggregates. Uses hourly aggregates for 24h/7d windows and daily aggregates for 30d/90d windows.", + "operationId": "getUptime", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "window", + "in": "query", + "description": "Time window for uptime calculation", + "required": false, + "schema": { + "type": "string", + "enum": [ + "24h", + "7d", + "30d", + "90d" + ] + } + } + ], + "responses": { + "200": { + "description": "Uptime statistics", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/UptimeDto" + } + } + } + }, + "400": { + "description": "Invalid window parameter", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseUptimeDto" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Monitor does not belong to the caller's org", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseUptimeDto" + } + } + } + }, + "404": { + "description": "Monitor not found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseUptimeDto" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/versions": { + "get": { + "tags": [ + "Monitors" + ], + "summary": "List version history for a monitor", + "description": "Returns a paginated list of mutation snapshots for the monitor, newest first. Each version captures the full monitor config at the time of a PUT /monitors/{id} call.", + "operationId": "listVersions", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultMonitorVersionDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/versions/{version}": { + "get": { + "tags": [ + "Monitors" + ], + "summary": "Get a specific version snapshot for a monitor", + "description": "Returns the full monitor config snapshot captured at the given version number.", + "operationId": "getVersion", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorVersionDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/{monitorId}/alert-channels": { + "put": { + "tags": [ + "Monitor Alert Channels" + ], + "summary": "Replace the linked alert channel set for a monitor", + "operationId": "setChannels", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetAlertChannelsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseListUUID" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/{monitorId}/assertions": { + "post": { + "tags": [ + "Monitor Assertions" + ], + "summary": "Add an assertion to a monitor", + "operationId": "add", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAssertionRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorAssertionDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/{monitorId}/assertions/{assertionId}": { + "put": { + "tags": [ + "Monitor Assertions" + ], + "summary": "Update an assertion on a monitor", + "operationId": "update_10", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "assertionId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAssertionRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorAssertionDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Monitor Assertions" + ], + "summary": "Remove an assertion from a monitor", + "operationId": "remove_1", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "assertionId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/{monitorId}/auth": { + "put": { + "tags": [ + "Monitor Auth" + ], + "summary": "Update authentication config for a monitor", + "operationId": "update_9", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMonitorAuthRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorAuthDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "Monitor Auth" + ], + "summary": "Set authentication config for a monitor", + "operationId": "set", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetMonitorAuthRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorAuthDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Monitor Auth" + ], + "summary": "Remove authentication config from a monitor", + "operationId": "remove", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/{monitorId}/policy": { + "get": { + "tags": [ + "Incident Policies" + ], + "summary": "Get incident policy for a monitor", + "description": "Returns the trigger rules, confirmation settings, and recovery settings for the given monitor.", + "operationId": "get_5", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "description": "Monitor UUID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Policy found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/IncidentPolicyDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Monitor or policy not found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentPolicyDto" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "put": { + "tags": [ + "Incident Policies" + ], + "summary": "Update incident policy for a monitor", + "description": "Replaces the trigger rules, confirmation settings, and recovery settings. All fields are validated before saving.", + "operationId": "update_8", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "description": "Monitor UUID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateIncidentPolicyRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Policy updated", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/IncidentPolicyDto" + } + } + } + }, + "400": { + "description": "Validation error in JSONB shape", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentPolicyDto" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Monitor or policy not found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentPolicyDto" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/bulk": { + "post": { + "tags": [ + "Monitors" + ], + "summary": "Bulk action on monitors", + "description": "Applies PAUSE, RESUME, DELETE, ADD_TAG, or REMOVE_TAG to a list of monitors. Returns a partial-success response indicating which monitors succeeded and which failed.", + "operationId": "bulkAction", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkMonitorActionRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseBulkMonitorActionResult" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/monitors/test": { + "post": { + "tags": [ + "Monitors" + ], + "summary": "Ad-hoc monitor test", + "description": "Executes a one-off check from an inline config without saving the monitor. Runs synchronously and returns status code, response time, assertion results, body preview, and headers.", + "operationId": "testAdHoc", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MonitorTestRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorTestResultDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/notification-dispatches": { + "get": { + "tags": [ + "Notification Dispatches" + ], + "summary": "List all dispatches for an incident", + "description": "Returns all notification dispatches for the given incident that belong to the authenticated org's policies. Each dispatch includes delivery records for all associated channels.", + "operationId": "listByIncident", + "parameters": [ + { + "name": "incident_id", + "in": "query", + "description": "UUID of the incident to inspect", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultNotificationDispatchDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/notification-dispatches/{id}": { + "get": { + "tags": [ + "Notification Dispatches" + ], + "summary": "Get a single dispatch with full escalation and delivery history", + "description": "Returns the dispatch state including current escalation step, acknowledgment info, and all delivery attempts made across every step.", + "operationId": "getById_3", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseNotificationDispatchDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/notification-dispatches/{id}/acknowledge": { + "post": { + "tags": [ + "Notification Dispatches" + ], + "summary": "Acknowledge a notification dispatch", + "description": "Marks the dispatch as acknowledged. The dispatch must be in DELIVERED or ESCALATING state. Sets acknowledgedAt, acknowledgedBy (actor email), and acknowledgedVia (DASHBOARD).", + "operationId": "acknowledge", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseNotificationDispatchDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/notification-policies": { + "get": { + "tags": [ + "Notification Policies" + ], + "summary": "List all notification policies for the authenticated org", + "operationId": "list_7", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultNotificationPolicyDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "Notification Policies" + ], + "summary": "Create a notification policy with match rules and escalation chain", + "operationId": "create_8", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateNotificationPolicyRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseNotificationPolicyDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/notification-policies/{id}": { + "get": { + "tags": [ + "Notification Policies" + ], + "summary": "Get a notification policy by ID", + "operationId": "getById_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseNotificationPolicyDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "put": { + "tags": [ + "Notification Policies" + ], + "summary": "Update a notification policy", + "operationId": "update_7", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateNotificationPolicyRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseNotificationPolicyDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Notification Policies" + ], + "summary": "Delete a notification policy", + "operationId": "delete_6", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/notification-policies/{id}/dispatches": { + "get": { + "tags": [ + "Notification Policies" + ], + "summary": "List all dispatches (firing history) for a notification policy", + "operationId": "listDispatches", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultNotificationDispatchDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/notification-policies/{id}/test": { + "post": { + "tags": [ + "Notification Policies" + ], + "summary": "Dry-run: evaluate a policy's match rules against a supplied incident context", + "operationId": "test_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestNotificationPolicyRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseTestMatchResult" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/notifications": { + "get": { + "tags": [ + "Notifications" + ], + "summary": "List notifications for the current user", + "operationId": "list_16", + "parameters": [ + { + "name": "unreadOnly", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultNotificationDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/notifications/{id}/read": { + "put": { + "tags": [ + "Notifications" + ], + "summary": "Mark a notification as read", + "operationId": "markRead", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/notifications/read-all": { + "put": { + "tags": [ + "Notifications" + ], + "summary": "Mark all notifications as read", + "operationId": "markAllRead", + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/notifications/unread-count": { + "get": { + "tags": [ + "Notifications" + ], + "summary": "Get unread notification count", + "operationId": "unreadCount", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseLong" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/org": { + "get": { + "tags": [ + "Organizations" + ], + "summary": "Get the current organization", + "operationId": "get_4", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseOrganizationDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "put": { + "tags": [ + "Organizations" + ], + "summary": "Update the current organization", + "operationId": "update_6", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgDetailsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseOrganizationDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/resource-groups": { + "get": { + "tags": [ + "Resource Groups" + ], + "summary": "List all resource groups for the authenticated org with health summaries", + "operationId": "list_6", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultResourceGroupDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "Resource Groups" + ], + "summary": "Create a new resource group", + "operationId": "create_7", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateResourceGroupRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseResourceGroupDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/resource-groups/{id}": { + "get": { + "tags": [ + "Resource Groups" + ], + "summary": "Get a resource group by id with member statuses and inherited settings", + "description": "Pass includeMetrics=true to enrich each member with 24h uptime, chart data, and latency metrics.", + "operationId": "get_3", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "includeMetrics", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseResourceGroupDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "put": { + "tags": [ + "Resource Groups" + ], + "summary": "Update a resource group's name, description, alert policy, inherited settings, and health threshold", + "operationId": "update_5", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateResourceGroupRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseResourceGroupDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Resource Groups" + ], + "summary": "Delete a resource group (cascades to member rows)", + "operationId": "delete_5", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/resource-groups/{id}/health": { + "get": { + "tags": [ + "Resource Groups" + ], + "summary": "Get the detailed health breakdown for a resource group", + "description": "Returns member counts, worst-of status, and threshold-based health evaluation. The thresholdStatus field is populated only when a health threshold is configured.", + "operationId": "getHealth", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseResourceGroupHealthDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/resource-groups/{id}/members": { + "post": { + "tags": [ + "Resource Groups" + ], + "summary": "Add a monitor or service member to a resource group", + "operationId": "addMember_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddResourceGroupMemberRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseResourceGroupMemberDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/resource-groups/{id}/members/{memberId}": { + "delete": { + "tags": [ + "Resource Groups" + ], + "summary": "Remove a member from a resource group", + "operationId": "removeMember_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "memberId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/secrets": { + "get": { + "tags": [ + "Secrets" + ], + "summary": "List secrets", + "operationId": "list_5", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultSecretDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "Secrets" + ], + "summary": "Create secret", + "operationId": "create_6", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSecretRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseSecretDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/secrets/{key}": { + "put": { + "tags": [ + "Secrets" + ], + "summary": "Update secret", + "operationId": "update_4", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSecretRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseSecretDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Secrets" + ], + "summary": "Delete secret", + "operationId": "delete_4", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/service-subscriptions": { + "get": { + "tags": [ + "Service Subscriptions" + ], + "summary": "List all service subscriptions for the organization", + "operationId": "list_15", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultServiceSubscriptionDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/service-subscriptions/{id}": { + "get": { + "tags": [ + "Service Subscriptions" + ], + "summary": "Get a subscription by its ID", + "operationId": "get_10", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseServiceSubscriptionDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Service Subscriptions" + ], + "summary": "Remove a subscription by its ID", + "description": "Removes a specific subscription (whole-service or component-level). No-op if not found.", + "operationId": "unsubscribe_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/service-subscriptions/{id}/alert-sensitivity": { + "patch": { + "tags": [ + "Service Subscriptions" + ], + "summary": "Update alert sensitivity for a subscription", + "description": "Controls which external incidents trigger alerts: ALL (any status change), INCIDENTS_ONLY (real vendor incidents, default), MAJOR_ONLY (only DOWN-level incidents).", + "operationId": "updateAlertSensitivity", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAlertSensitivityRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseServiceSubscriptionDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/service-subscriptions/{slug}": { + "post": { + "tags": [ + "Service Subscriptions" + ], + "summary": "Subscribe to a service or a component of a service", + "description": "Idempotent — returns the existing subscription if an identical one exists. Omit the request body or set componentId to null for a whole-service subscription. Free tier: max 10 subscriptions. Paid tier: unlimited.", + "operationId": "subscribe_1", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceSubscribeRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseServiceSubscriptionDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/services": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "List all enabled services (cursor-paginated)", + "operationId": "listServices", + "parameters": [ + { + "name": "category", + "in": "query", + "description": "Filter by category (exact match)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "description": "Filter by current overall_status (exact match)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "published", + "in": "query", + "description": "Filter by published status for pSEO pages", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "cursor", + "in": "query", + "description": "Opaque cursor from a previous response", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Page size (1–100, default 20)", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CursorPageServiceCatalogDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "Get a single service by slug or UUID with current status, components, and recent incidents", + "operationId": "getService", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseServiceDetailDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}/components": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "List active components for a service with current status and inline uptime", + "operationId": "getComponents", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultServiceComponentDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}/components/{componentId}/uptime": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "Get daily uptime data for a component", + "description": "Pass either ``period`` (preset window) or an explicit ``from``/``to`` calendar window (ISO yyyy-MM-dd, max 730 days, ``to`` defaults to today). When both are supplied, the explicit window wins.", + "operationId": "getComponentUptime", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "componentId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "period", + "in": "query", + "description": "Preset time window (used when ``from`` is omitted)", + "required": false, + "schema": { + "type": "string", + "enum": [ + "7d", + "30d", + "90d", + "1y" + ] + } + }, + { + "name": "from", + "in": "query", + "description": "Explicit window start (ISO date); overrides ``period``", + "required": false, + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "to", + "in": "query", + "description": "Explicit window end (ISO date); defaults to today", + "required": false, + "schema": { + "type": "string", + "format": "date" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultComponentUptimeDayDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}/components/uptime": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "Batch daily uptime data for all leaf components", + "description": "Returns daily uptime for every active non-group component with uptime data, keyed by component ID. Replaces N individual per-component uptime calls with a single request. Supports either a preset ``period`` or an explicit ``from``/``to`` window (ISO yyyy-MM-dd, max 730 days). The explicit window wins when both are supplied — this is what powers the sliding 90-day navigator on the public uptime history page.", + "operationId": "getBatchComponentUptime", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "period", + "in": "query", + "description": "Preset time window (used when ``from`` is omitted)", + "required": false, + "schema": { + "type": "string", + "enum": [ + "7d", + "30d", + "90d", + "1y" + ] + } + }, + { + "name": "from", + "in": "query", + "description": "Explicit window start (ISO date); overrides ``period``", + "required": false, + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "to", + "in": "query", + "description": "Explicit window end (ISO date); defaults to today", + "required": false, + "schema": { + "type": "string", + "format": "date" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseBatchComponentUptimeDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}/days/{date}": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "One-day rollup for a service: aggregated uptime, per-component impacts, and overlapping incidents", + "description": "Powers the click/hover-to-expand panel under each uptime bar on the public status page. Single round-trip — components, sums, and overlapping incidents (with affected component names) are returned in one response.", + "operationId": "getServiceDayDetail", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "date", + "in": "path", + "description": "UTC calendar day in ISO format (YYYY-MM-DD)", + "required": true, + "schema": { + "type": "string", + "format": "date" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseServiceDayDetailDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}/incidents": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "List incident history for a service (paginated)", + "operationId": "listIncidents_1", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "from", + "in": "query", + "description": "Earliest start date (ISO 8601 date)", + "required": false, + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "status", + "in": "query", + "description": "Filter: active (unresolved), resolved, or omit for all", + "required": false, + "schema": { + "type": "string", + "enum": [ + "active", + "resolved" + ] + } + }, + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultServiceIncidentDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}/incidents/{incidentId}": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "Get incident detail with full update timeline", + "operationId": "getIncident_1", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "incidentId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseServiceIncidentDetailDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}/live-status": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "Lightweight live-status snapshot for polling", + "description": "Returns only the current overall status, component statuses, and active incident count. Designed for frequent polling with minimal payload.", + "operationId": "getServiceLiveStatus", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseServiceLiveStatusDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}/maintenances": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "List scheduled maintenances for a service", + "operationId": "getScheduledMaintenances", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "description": "Filter by status (e.g. scheduled, in_progress, verifying, completed)", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultScheduledMaintenanceDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}/poll-results": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "List poll results for a service (cursor-paginated)", + "operationId": "listPollResults", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cursor", + "in": "query", + "description": "ISO 8601 timestamp cursor from a previous response", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Page size (1–100, default 50)", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 50 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CursorPageServicePollResultDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}/poll-summary": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "Get aggregated poll metrics and chart data for a service", + "operationId": "getPollSummary", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "window", + "in": "query", + "description": "Time window", + "required": false, + "schema": { + "type": "string", + "enum": [ + "24h", + "7d", + "30d" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseServicePollSummaryDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}/uptime": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "Get uptime statistics for a service", + "description": "Uptime data aggregated across active non-group components.", + "operationId": "getServiceUptime", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "period", + "in": "query", + "description": "Time window", + "required": false, + "schema": { + "type": "string", + "enum": [ + "24h", + "7d", + "30d", + "90d", + "1y", + "2y", + "all" + ] + } + }, + { + "name": "granularity", + "in": "query", + "description": "Bucket granularity", + "required": false, + "schema": { + "type": "string", + "enum": [ + "hourly", + "daily", + "monthly" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseServiceUptimeResponse" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "BearerAuth": [] + } + ] + } + }, + "/api/v1/services/incidents": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "List vendor incidents across all services (paginated)", + "description": "Cross-service vendor incident feed ordered by start date descending.", + "operationId": "listCrossServiceIncidents", + "parameters": [ + { + "name": "from", + "in": "query", + "description": "Earliest start date (ISO 8601 date)", + "required": false, + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "status", + "in": "query", + "description": "Filter: active (unresolved), resolved, or omit for all", + "required": false, + "schema": { + "type": "string", + "enum": [ + "active", + "resolved" + ] + } + }, + { + "name": "category", + "in": "query", + "description": "Filter by service category", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultServiceIncidentDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/services/summary": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "Global status summary across all services", + "description": "Returns aggregate counts of services by status and a list of services currently experiencing issues.", + "operationId": "getGlobalStatusSummary", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseGlobalStatusSummaryDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/status-pages": { + "get": { + "tags": [ + "Status Pages" + ], + "summary": "List status pages for the workspace", + "operationId": "list_4", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultStatusPageDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "tags": [ + "Status Pages" + ], + "summary": "Create a status page", + "operationId": "create_5", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateStatusPageRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseStatusPageDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/status-pages/{id}": { + "get": { "tags": [ - "Members" + "Status Pages" ], - "summary": "Remove member from organization", - "operationId": "remove_2", + "summary": "Get a status page", + "operationId": "get_2", "parameters": [ { - "name": "userId", + "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseStatusPageDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "put": { + "tags": [ + "Status Pages" + ], + "summary": "Update a status page", + "operationId": "update_3", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateStatusPageRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseStatusPageDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Status Pages" + ], + "summary": "Delete a status page", + "operationId": "delete_3", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } } ], "responses": { "204": { "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } }, - "/api/v1/members/{userId}/role": { - "put": { + "/api/v1/status-pages/{id}/components": { + "get": { "tags": [ - "Members" + "Status Pages" ], - "summary": "Change member role", - "operationId": "changeRole", + "summary": "List all components", + "operationId": "listComponents", "parameters": [ { - "name": "userId", + "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string", + "format": "uuid" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChangeRoleRequest" + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultStatusPageComponentDto" + } } } }, - "required": true - }, - "responses": { - "204": { - "description": "No Content" + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } - } - }, - "/api/v1/members/{userId}/status": { - "put": { + }, + "post": { "tags": [ - "Members" + "Status Pages" ], - "summary": "Change member status", - "operationId": "changeStatus", + "summary": "Add a component to the status page", + "operationId": "createComponent", "parameters": [ { - "name": "userId", + "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string", + "format": "uuid" } } ], @@ -1688,140 +15387,99 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ChangeStatusRequest" + "$ref": "#/components/schemas/CreateStatusPageComponentRequest" } } }, "required": true }, "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/monitors": { - "get": { - "tags": [ - "Monitors" - ], - "summary": "List monitors for the authenticated org", - "operationId": "list_8", - "parameters": [ - { - "name": "enabled", - "in": "query", - "description": "Filter by enabled state", - "required": false, - "schema": { - "type": "boolean" + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseStatusPageComponentDto" + } + } } }, - { - "name": "type", - "in": "query", - "description": "Filter by monitor type", - "required": false, - "schema": { - "type": "string", - "enum": [ - "HTTP", - "DNS", - "MCP_SERVER", - "TCP", - "ICMP", - "HEARTBEAT" - ] + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "managedBy", - "in": "query", - "description": "Filter by managed-by source", - "required": false, - "schema": { - "type": "string", - "enum": [ - "DASHBOARD", - "CLI", - "TERRAFORM" - ] + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "tags", - "in": "query", - "description": "Filter by tag names, comma-separated (e.g. prod,critical)", - "required": false, - "schema": { - "type": "string" + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "search", - "in": "query", - "description": "Case-insensitive name search", - "required": false, - "schema": { - "type": "string" + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "environmentId", - "in": "query", - "description": "Filter by environment ID", - "required": false, - "schema": { - "type": "string", - "format": "uuid" + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" - } - } - ], - "responses": { - "200": { - "description": "OK", + "500": { + "description": "Internal server error — see the message field for details", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultMonitorDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "post": { - "tags": [ - "Monitors" - ], - "summary": "Create a new monitor", - "operationId": "create_9", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateMonitorRequest" + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "503": { + "description": "Service unavailable — try again shortly", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -1829,13 +15487,13 @@ } } }, - "/api/v1/monitors/{id}": { - "get": { + "/api/v1/status-pages/{id}/components/{componentId}": { + "put": { "tags": [ - "Monitors" + "Status Pages" ], - "summary": "Get a single monitor by id", - "operationId": "get_6", + "summary": "Update a component", + "operationId": "updateComponent", "parameters": [ { "name": "id", @@ -1845,30 +15503,9 @@ "type": "string", "format": "uuid" } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorDto" - } - } - } - } - } - }, - "put": { - "tags": [ - "Monitors" - ], - "summary": "Update a monitor", - "operationId": "update_11", - "parameters": [ + }, { - "name": "id", + "name": "componentId", "in": "path", "required": true, "schema": { @@ -1881,7 +15518,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateMonitorRequest" + "$ref": "#/components/schemas/UpdateStatusPageComponentRequest" } } }, @@ -1893,7 +15530,87 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + "$ref": "#/components/schemas/SingleValueResponseStatusPageComponentDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -1902,10 +15619,10 @@ }, "delete": { "tags": [ - "Monitors" + "Status Pages" ], - "summary": "Soft-delete a monitor", - "operationId": "delete_7", + "summary": "Remove a component from the status page", + "operationId": "deleteComponent", "parameters": [ { "name": "id", @@ -1915,25 +15632,9 @@ "type": "string", "format": "uuid" } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/monitors/{id}/pause": { - "post": { - "tags": [ - "Monitors" - ], - "summary": "Pause a monitor (set enabled=false)", - "operationId": "pause", - "parameters": [ + }, { - "name": "id", + "name": "componentId", "in": "path", "required": true, "schema": { @@ -1943,134 +15644,85 @@ } ], "responses": { - "200": { - "description": "OK", + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/monitors/{id}/results": { - "get": { - "tags": [ - "Check Results" - ], - "summary": "List raw check results", - "description": "Returns check results for the given monitor with optional time-range, region, and pass/fail filtering. Uses cursor-based pagination — pass the returned `cursor` value on subsequent requests to retrieve the next page. The cursor encodes the original time bounds, so `from`/`to` are ignored when a cursor is present.", - "operationId": "getResults", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "from", - "in": "query", - "description": "Start of time range (ISO 8601, inclusive); defaults to 24 hours ago", - "required": false, - "schema": { - "type": "string", - "format": "date-time" - } }, - { - "name": "to", - "in": "query", - "description": "End of time range (ISO 8601, inclusive); defaults to now", - "required": false, - "schema": { - "type": "string", - "format": "date-time" + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "cursor", - "in": "query", - "description": "Opaque cursor from a previous response for pagination", - "required": false, - "schema": { - "type": "string" + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "limit", - "in": "query", - "description": "Maximum results per page (1–200)", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 50 - }, - "example": 50 - }, - { - "name": "region", - "in": "query", - "description": "Filter by region (e.g. us-east)", - "required": false, - "schema": { - "type": "string" + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "passed", - "in": "query", - "description": "Filter by pass/fail status", - "required": false, - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "Paginated check results", + "409": { + "description": "Conflict — the request collides with current resource state", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/CursorPage" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "400": { - "description": "Invalid query parameters", + "500": { + "description": "Internal server error — see the message field for details", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/CursorPageCheckResultDto" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "403": { - "description": "Monitor does not belong to the caller's org", + "502": { + "description": "Bad gateway — an upstream provider returned an error", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/CursorPageCheckResultDto" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "404": { - "description": "Monitor not found", + "503": { + "description": "Service unavailable — try again shortly", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/CursorPageCheckResultDto" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -2078,14 +15730,13 @@ } } }, - "/api/v1/monitors/{id}/results/summary": { + "/api/v1/status-pages/{id}/components/{componentId}/uptime": { "get": { "tags": [ - "Check Results" + "Status Pages" ], - "summary": "Get results summary", - "description": "Returns a dashboard summary for the monitor: current status derived from the latest result per region, time-bucketed chart data, the 24-hour uptime percentage, and the selected window's uptime percentage.", - "operationId": "getSummary", + "summary": "Get component uptime history (daily rollups)", + "operationId": "componentUptime_1", "parameters": [ { "name": "id", @@ -2097,90 +15748,112 @@ } }, { - "name": "chartWindow", + "name": "componentId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "days", "in": "query", - "description": "Chart window: 24h returns hourly buckets, 7d/30d/90d return daily buckets", "required": false, "schema": { - "type": "string", - "enum": [ - "24h", - "7d", - "30d", - "90d" - ] + "type": "integer", + "format": "int32", + "default": 90 } } ], "responses": { "200": { - "description": "Results summary", + "description": "OK", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/ResultSummaryDto" + "$ref": "#/components/schemas/TableValueResultStatusPageComponentUptimeDayDto" } } } }, "400": { - "description": "Invalid chartWindow parameter", + "description": "Bad request — the payload failed validation", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseResultSummaryDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { - "description": "Monitor does not belong to the caller's org", + "description": "Forbidden — the actor lacks permission for this resource", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseResultSummaryDto" + "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { - "description": "Monitor not found", + "description": "Not found — the requested resource does not exist", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseResultSummaryDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/monitors/{id}/resume": { - "post": { - "tags": [ - "Monitors" - ], - "summary": "Resume a monitor (set enabled=true)", - "operationId": "resume", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "500": { + "description": "Internal server error — see the message field for details", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -2188,14 +15861,13 @@ } } }, - "/api/v1/monitors/{id}/rotate-token": { - "post": { + "/api/v1/status-pages/{id}/components/reorder": { + "put": { "tags": [ - "Monitors" + "Status Pages" ], - "summary": "Rotate the ping token for a heartbeat monitor", - "description": "Generates a new ping token. The old token remains valid for 24 hours to allow cron jobs to be updated without downtime. Only supported for HEARTBEAT monitors.", - "operationId": "rotateToken", + "summary": "Batch reorder components (and optionally move between groups)", + "operationId": "reorderComponents", "parameters": [ { "name": "id", @@ -2207,13 +15879,96 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReorderComponentsRequest" + } + } + }, + "required": true + }, "responses": { - "200": { - "description": "OK", + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -2221,13 +15976,13 @@ } } }, - "/api/v1/monitors/{id}/tags": { + "/api/v1/status-pages/{id}/domains": { "get": { "tags": [ - "Monitors" + "Status Pages" ], - "summary": "Get all tags applied to a monitor", - "operationId": "getMonitorTags", + "summary": "List custom domains", + "operationId": "listDomains", "parameters": [ { "name": "id", @@ -2245,59 +16000,99 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/TableValueResultTagDto" + "$ref": "#/components/schemas/TableValueResultStatusPageCustomDomainDto" } } } - } - } - }, - "post": { - "tags": [ - "Monitors" - ], - "summary": "Add tags to a monitor; supports existing tag IDs and inline creation of new tags", - "operationId": "addMonitorTags", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AddMonitorTagsRequest" + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultTagDto" + "$ref": "#/components/schemas/ErrorResponse" } } } } } }, - "delete": { + "post": { "tags": [ - "Monitors" + "Status Pages" ], - "summary": "Remove tags from a monitor by their IDs", - "operationId": "removeMonitorTags", + "summary": "Add a custom domain", + "operationId": "addDomain", "parameters": [ { "name": "id", @@ -2313,164 +16108,99 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RemoveMonitorTagsRequest" + "$ref": "#/components/schemas/AddCustomDomainRequest" } } }, "required": true }, "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/monitors/{id}/test": { - "post": { - "tags": [ - "Monitors" - ], - "summary": "Test an existing monitor", - "description": "Runs the saved config and assertions of an existing monitor once, without persisting any result. Runs synchronously and returns the same shape as the ad-hoc test.", - "operationId": "testExisting", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorTestResultDto" + "$ref": "#/components/schemas/SingleValueResponseStatusPageCustomDomainDto" } } } - } - } - } - }, - "/api/v1/monitors/{id}/uptime": { - "get": { - "tags": [ - "Check Results" - ], - "summary": "Get uptime statistics", - "description": "Returns uptime percentage and latency statistics for the requested time window, computed from continuous aggregates. Uses hourly aggregates for 24h/7d windows and daily aggregates for 30d/90d windows.", - "operationId": "getUptime", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } }, - { - "name": "window", - "in": "query", - "description": "Time window for uptime calculation", - "required": false, - "schema": { - "type": "string", - "enum": [ - "24h", - "7d", - "30d", - "90d" - ] - } - } - ], - "responses": { - "200": { - "description": "Uptime statistics", + "400": { + "description": "Bad request — the payload failed validation", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/UptimeDto" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "400": { - "description": "Invalid window parameter", + "401": { + "description": "Unauthorized — missing or invalid credentials", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseUptimeDto" + "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { - "description": "Monitor does not belong to the caller's org", + "description": "Forbidden — the actor lacks permission for this resource", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseUptimeDto" + "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { - "description": "Monitor not found", + "description": "Not found — the requested resource does not exist", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseUptimeDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/monitors/{id}/versions": { - "get": { - "tags": [ - "Monitors" - ], - "summary": "List version history for a monitor", - "description": "Returns a paginated list of mutation snapshots for the monitor, newest first. Each version captures the full monitor config at the time of a PUT /monitors/{id} call.", - "operationId": "listVersions", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultMonitorVersionDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -2478,14 +16208,13 @@ } } }, - "/api/v1/monitors/{id}/versions/{version}": { - "get": { + "/api/v1/status-pages/{id}/domains/{domainId}": { + "delete": { "tags": [ - "Monitors" + "Status Pages" ], - "summary": "Get a specific version snapshot for a monitor", - "description": "Returns the full monitor config snapshot captured at the given version number.", - "operationId": "getVersion", + "summary": "Remove a custom domain", + "operationId": "removeDomain", "parameters": [ { "name": "id", @@ -2497,106 +16226,95 @@ } }, { - "name": "version", + "name": "domainId", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string", + "format": "uuid" } } ], "responses": { - "200": { - "description": "OK", + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorVersionDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/monitors/{monitorId}/alert-channels": { - "put": { - "tags": [ - "Monitor Alert Channels" - ], - "summary": "Replace the linked alert channel set for a monitor", - "operationId": "setChannels", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SetAlertChannelsRequest" + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "404": { + "description": "Not found — the requested resource does not exist", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseListUUID" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/monitors/{monitorId}/assertions": { - "post": { - "tags": [ - "Monitor Assertions" - ], - "summary": "Add an assertion to a monitor", - "operationId": "add", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateAssertionRequest" + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "503": { + "description": "Service unavailable — try again shortly", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorAssertionDto" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -2604,16 +16322,16 @@ } } }, - "/api/v1/monitors/{monitorId}/assertions/{assertionId}": { - "put": { + "/api/v1/status-pages/{id}/domains/{domainId}/verify": { + "post": { "tags": [ - "Monitor Assertions" + "Status Pages" ], - "summary": "Update an assertion on a monitor", - "operationId": "update_10", + "summary": "Trigger domain verification check", + "operationId": "verifyDomain", "parameters": [ { - "name": "monitorId", + "name": "id", "in": "path", "required": true, "schema": { @@ -2622,7 +16340,7 @@ } }, { - "name": "assertionId", + "name": "domainId", "in": "path", "required": true, "schema": { @@ -2631,180 +16349,111 @@ } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateAssertionRequest" - } - } - }, - "required": true - }, "responses": { "200": { "description": "OK", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorAssertionDto" + "$ref": "#/components/schemas/SingleValueResponseStatusPageCustomDomainDto" } } } - } - } - }, - "delete": { - "tags": [ - "Monitor Assertions" - ], - "summary": "Remove an assertion from a monitor", - "operationId": "remove_1", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } }, - { - "name": "assertionId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/monitors/{monitorId}/auth": { - "put": { - "tags": [ - "Monitor Auth" - ], - "summary": "Update authentication config for a monitor", - "operationId": "update_9", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateMonitorAuthRequest" + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "404": { + "description": "Not found — the requested resource does not exist", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorAuthDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "post": { - "tags": [ - "Monitor Auth" - ], - "summary": "Set authentication config for a monitor", - "operationId": "set", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SetMonitorAuthRequest" + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "502": { + "description": "Bad gateway — an upstream provider returned an error", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorAuthDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "delete": { - "tags": [ - "Monitor Auth" - ], - "summary": "Remove authentication config from a monitor", - "operationId": "remove", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } } - ], - "responses": { - "204": { - "description": "No Content" - } } } }, - "/api/v1/monitors/{monitorId}/policy": { + "/api/v1/status-pages/{id}/groups": { "get": { "tags": [ - "Incident Policies" + "Status Pages" ], - "summary": "Get incident policy for a monitor", - "description": "Returns the trigger rules, confirmation settings, and recovery settings for the given monitor.", - "operationId": "get_5", + "summary": "List component groups with nested components", + "operationId": "listGroups", "parameters": [ { - "name": "monitorId", + "name": "id", "in": "path", - "description": "Monitor UUID", "required": true, "schema": { "type": "string", @@ -2814,39 +16463,107 @@ ], "responses": { "200": { - "description": "Policy found", + "description": "OK", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/IncidentPolicyDto" + "$ref": "#/components/schemas/TableValueResultStatusPageComponentGroupDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { - "description": "Monitor or policy not found", + "description": "Not found — the requested resource does not exist", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentPolicyDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } } } }, - "put": { + "post": { "tags": [ - "Incident Policies" + "Status Pages" ], - "summary": "Update incident policy for a monitor", - "description": "Replaces the trigger rules, confirmation settings, and recovery settings. All fields are validated before saving.", - "operationId": "update_8", + "summary": "Create a component group", + "operationId": "createGroup", "parameters": [ { - "name": "monitorId", + "name": "id", "in": "path", - "description": "Monitor UUID", "required": true, "schema": { "type": "string", @@ -2858,137 +16575,99 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateIncidentPolicyRequest" + "$ref": "#/components/schemas/CreateStatusPageComponentGroupRequest" } } }, "required": true }, "responses": { - "200": { - "description": "Policy updated", + "201": { + "description": "Created", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/IncidentPolicyDto" + "$ref": "#/components/schemas/SingleValueResponseStatusPageComponentGroupDto" } } } }, "400": { - "description": "Validation error in JSONB shape", + "description": "Bad request — the payload failed validation", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentPolicyDto" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "404": { - "description": "Monitor or policy not found", + "401": { + "description": "Unauthorized — missing or invalid credentials", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentPolicyDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/monitors/bulk": { - "post": { - "tags": [ - "Monitors" - ], - "summary": "Bulk action on monitors", - "description": "Applies PAUSE, RESUME, DELETE, ADD_TAG, or REMOVE_TAG to a list of monitors. Returns a partial-success response indicating which monitors succeeded and which failed.", - "operationId": "bulkAction", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkMonitorActionRequest" + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "404": { + "description": "Not found — the requested resource does not exist", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseBulkMonitorActionResult" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/monitors/test": { - "post": { - "tags": [ - "Monitors" - ], - "summary": "Ad-hoc monitor test", - "description": "Executes a one-off check from an inline config without saving the monitor. Runs synchronously and returns status code, response time, assertion results, body preview, and headers.", - "operationId": "testAdHoc", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MonitorTestRequest" + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "500": { + "description": "Internal server error — see the message field for details", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorTestResultDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/notification-dispatches": { - "get": { - "tags": [ - "Notification Dispatches" - ], - "summary": "List all dispatches for an incident", - "description": "Returns all notification dispatches for the given incident that belong to the authenticated org's policies. Each dispatch includes delivery records for all associated channels.", - "operationId": "listByIncident", - "parameters": [ - { - "name": "incident_id", - "in": "query", - "description": "UUID of the incident to inspect", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "503": { + "description": "Service unavailable — try again shortly", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultNotificationDispatchDto" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -2996,14 +16675,13 @@ } } }, - "/api/v1/notification-dispatches/{id}": { - "get": { + "/api/v1/status-pages/{id}/groups/{groupId}": { + "put": { "tags": [ - "Notification Dispatches" + "Status Pages" ], - "summary": "Get a single dispatch with full escalation and delivery history", - "description": "Returns the dispatch state including current escalation step, acknowledgment info, and all delivery attempts made across every step.", - "operationId": "getById_3", + "summary": "Update a component group", + "operationId": "updateGroup", "parameters": [ { "name": "id", @@ -3013,33 +16691,9 @@ "type": "string", "format": "uuid" } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseNotificationDispatchDto" - } - } - } - } - } - } - }, - "/api/v1/notification-dispatches/{id}/acknowledge": { - "post": { - "tags": [ - "Notification Dispatches" - ], - "summary": "Acknowledge a notification dispatch", - "description": "Marks the dispatch as acknowledged. The dispatch must be in DELIVERED or ESCALATING state. Sets acknowledgedAt, acknowledgedBy (actor email), and acknowledgedVia (DASHBOARD).", - "operationId": "acknowledge", - "parameters": [ + }, { - "name": "id", + "name": "groupId", "in": "path", "required": true, "schema": { @@ -3048,135 +16702,103 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateStatusPageComponentGroupRequest" + } + } + }, + "required": true + }, "responses": { "200": { "description": "OK", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseNotificationDispatchDto" + "$ref": "#/components/schemas/SingleValueResponseStatusPageComponentGroupDto" } } } - } - } - } - }, - "/api/v1/notification-policies": { - "get": { - "tags": [ - "Notification Policies" - ], - "summary": "List all notification policies for the authenticated org", - "operationId": "list_7", - "responses": { - "200": { - "description": "OK", + }, + "400": { + "description": "Bad request — the payload failed validation", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultNotificationPolicyDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "post": { - "tags": [ - "Notification Policies" - ], - "summary": "Create a notification policy with match rules and escalation chain", - "operationId": "create_8", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateNotificationPolicyRequest" + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "403": { + "description": "Forbidden — the actor lacks permission for this resource", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseNotificationPolicyDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/notification-policies/{id}": { - "get": { - "tags": [ - "Notification Policies" - ], - "summary": "Get a notification policy by ID", - "operationId": "getById_1", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "409": { + "description": "Conflict — the request collides with current resource state", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseNotificationPolicyDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "put": { - "tags": [ - "Notification Policies" - ], - "summary": "Update a notification policy", - "operationId": "update_7", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateNotificationPolicyRequest" + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "503": { + "description": "Service unavailable — try again shortly", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseNotificationPolicyDto" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -3185,10 +16807,10 @@ }, "delete": { "tags": [ - "Notification Policies" + "Status Pages" ], - "summary": "Delete a notification policy", - "operationId": "delete_6", + "summary": "Delete a component group (components become ungrouped)", + "operationId": "deleteGroup", "parameters": [ { "name": "id", @@ -3198,22 +16820,111 @@ "type": "string", "format": "uuid" } + }, + { + "name": "groupId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } } ], "responses": { "204": { "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } }, - "/api/v1/notification-policies/{id}/dispatches": { + "/api/v1/status-pages/{id}/incidents": { "get": { "tags": [ - "Notification Policies" + "Status Pages" ], - "summary": "List all dispatches (firing history) for a notification policy", - "operationId": "listDispatches", + "summary": "List incidents for this status page (filterable by status, paginated)", + "operationId": "listIncidents", "parameters": [ { "name": "id", @@ -3223,6 +16934,31 @@ "type": "string", "format": "uuid" } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "INVESTIGATING", + "IDENTIFIED", + "MONITORING", + "RESOLVED" + ] + } + } + }, + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } } ], "responses": { @@ -3231,21 +16967,99 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/TableValueResultNotificationDispatchDto" + "$ref": "#/components/schemas/TableValueResultStatusPageIncidentDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } } } - } - }, - "/api/v1/notification-policies/{id}/test": { + }, "post": { "tags": [ - "Notification Policies" + "Status Pages" ], - "summary": "Dry-run: evaluate a policy's match rules against a supplied incident context", - "operationId": "test_1", + "summary": "Create a status page incident (manual)", + "operationId": "createIncident", "parameters": [ { "name": "id", @@ -3261,71 +17075,99 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestNotificationPolicyRequest" + "$ref": "#/components/schemas/CreateStatusPageIncidentRequest" } } }, "required": true }, "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseTestMatchResult" + "$ref": "#/components/schemas/SingleValueResponseStatusPageIncidentDto" } } } - } - } - } - }, - "/api/v1/notifications": { - "get": { - "tags": [ - "Notifications" - ], - "summary": "List notifications for the current user", - "operationId": "list_16", - "parameters": [ - { - "name": "unreadOnly", - "in": "query", - "required": false, - "schema": { - "type": "boolean", - "default": false + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "page", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 0 + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "size", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 20 - } - } - ], - "responses": { - "200": { - "description": "OK", + "503": { + "description": "Service unavailable — try again shortly", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultNotificationDto" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -3333,174 +17175,132 @@ } } }, - "/api/v1/notifications/{id}/read": { - "put": { + "/api/v1/status-pages/{id}/incidents/{incidentId}": { + "get": { "tags": [ - "Notifications" + "Status Pages" ], - "summary": "Mark a notification as read", - "operationId": "markRead", + "summary": "Get incident details with timeline", + "operationId": "getIncident", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string", + "format": "uuid" + } + }, + { + "name": "incidentId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } } ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/notifications/read-all": { - "put": { - "tags": [ - "Notifications" - ], - "summary": "Mark all notifications as read", - "operationId": "markAllRead", - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/notifications/unread-count": { - "get": { - "tags": [ - "Notifications" - ], - "summary": "Get unread notification count", - "operationId": "unreadCount", "responses": { "200": { "description": "OK", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseLong" + "$ref": "#/components/schemas/SingleValueResponseStatusPageIncidentDto" } } } - } - } - } - }, - "/api/v1/org": { - "get": { - "tags": [ - "Organizations" - ], - "summary": "Get the current organization", - "operationId": "get_4", - "responses": { - "200": { - "description": "OK", + }, + "400": { + "description": "Bad request — the payload failed validation", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseOrganizationDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "put": { - "tags": [ - "Organizations" - ], - "summary": "Update the current organization", - "operationId": "update_6", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrgDetailsRequest" + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "403": { + "description": "Forbidden — the actor lacks permission for this resource", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseOrganizationDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/resource-groups": { - "get": { - "tags": [ - "Resource Groups" - ], - "summary": "List all resource groups for the authenticated org with health summaries", - "operationId": "list_6", - "responses": { - "200": { - "description": "OK", + }, + "404": { + "description": "Not found — the requested resource does not exist", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultResourceGroupDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "post": { - "tags": [ - "Resource Groups" - ], - "summary": "Create a new resource group", - "operationId": "create_7", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateResourceGroupRequest" + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "500": { + "description": "Internal server error — see the message field for details", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseResourceGroupDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } } } - } - }, - "/api/v1/resource-groups/{id}": { - "get": { + }, + "put": { "tags": [ - "Resource Groups" + "Status Pages" ], - "summary": "Get a resource group by id with member statuses and inherited settings", - "description": "Pass includeMetrics=true to enrich each member with 24h uptime, chart data, and latency metrics.", - "operationId": "get_3", + "summary": "Update an incident", + "operationId": "updateIncident", "parameters": [ { "name": "id", @@ -3512,62 +17312,112 @@ } }, { - "name": "includeMetrics", - "in": "query", - "required": false, + "name": "incidentId", + "in": "path", + "required": true, "schema": { - "type": "boolean", - "default": false + "type": "string", + "format": "uuid" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateStatusPageIncidentRequest" + } + } + }, + "required": true + }, "responses": { "200": { "description": "OK", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseResourceGroupDto" + "$ref": "#/components/schemas/SingleValueResponseStatusPageIncidentDto" } } } - } - } - }, - "put": { - "tags": [ - "Resource Groups" - ], - "summary": "Update a resource group's name, description, alert policy, inherited settings, and health threshold", - "operationId": "update_5", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateResourceGroupRequest" + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "503": { + "description": "Service unavailable — try again shortly", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseResourceGroupDto" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -3576,10 +17426,10 @@ }, "delete": { "tags": [ - "Resource Groups" + "Status Pages" ], - "summary": "Delete a resource group (cascades to member rows)", - "operationId": "delete_5", + "summary": "Delete an incident", + "operationId": "deleteIncident", "parameters": [ { "name": "id", @@ -3589,26 +17439,9 @@ "type": "string", "format": "uuid" } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/resource-groups/{id}/health": { - "get": { - "tags": [ - "Resource Groups" - ], - "summary": "Get the detailed health breakdown for a resource group", - "description": "Returns member counts, worst-of status, and threshold-based health evaluation. The thresholdStatus field is populated only when a health threshold is configured.", - "operationId": "getHealth", - "parameters": [ + }, { - "name": "id", + "name": "incidentId", "in": "path", "required": true, "schema": { @@ -3618,54 +17451,85 @@ } ], "responses": { - "200": { - "description": "OK", + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseResourceGroupHealthDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/resource-groups/{id}/members": { - "post": { - "tags": [ - "Resource Groups" - ], - "summary": "Add a monitor or service member to a resource group", - "operationId": "addMember_1", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AddResourceGroupMemberRequest" + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "404": { + "description": "Not found — the requested resource does not exist", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseResourceGroupMemberDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -3673,13 +17537,13 @@ } } }, - "/api/v1/resource-groups/{id}/members/{memberId}": { - "delete": { + "/api/v1/status-pages/{id}/incidents/{incidentId}/dismiss": { + "post": { "tags": [ - "Resource Groups" + "Status Pages" ], - "summary": "Remove a member from a resource group", - "operationId": "removeMember_1", + "summary": "Dismiss a draft incident (deletes it permanently)", + "operationId": "dismissIncident", "parameters": [ { "name": "id", @@ -3691,7 +17555,7 @@ } }, { - "name": "memberId", + "name": "incidentId", "in": "path", "required": true, "schema": { @@ -3703,182 +17567,97 @@ "responses": { "204": { "description": "No Content" - } - } - } - }, - "/api/v1/secrets": { - "get": { - "tags": [ - "Secrets" - ], - "summary": "List secrets", - "operationId": "list_5", - "responses": { - "200": { - "description": "OK", + }, + "400": { + "description": "Bad request — the payload failed validation", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultSecretDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "post": { - "tags": [ - "Secrets" - ], - "summary": "Create secret", - "operationId": "create_6", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSecretRequest" + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "403": { + "description": "Forbidden — the actor lacks permission for this resource", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseSecretDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/secrets/{key}": { - "put": { - "tags": [ - "Secrets" - ], - "summary": "Update secret", - "operationId": "update_4", - "parameters": [ - { - "name": "key", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateSecretRequest" + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "409": { + "description": "Conflict — the request collides with current resource state", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseSecretDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "delete": { - "tags": [ - "Secrets" - ], - "summary": "Delete secret", - "operationId": "delete_4", - "parameters": [ - { - "name": "key", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/service-subscriptions": { - "get": { - "tags": [ - "Service Subscriptions" - ], - "summary": "List all service subscriptions for the organization", - "operationId": "list_15", - "responses": { - "200": { - "description": "OK", + }, + "500": { + "description": "Internal server error — see the message field for details", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultServiceSubscriptionDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/service-subscriptions/{id}": { - "get": { - "tags": [ - "Service Subscriptions" - ], - "summary": "Get a subscription by its ID", - "operationId": "get_9", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "503": { + "description": "Service unavailable — try again shortly", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseServiceSubscriptionDto" + "$ref": "#/components/schemas/ErrorResponse" } } } } } - }, - "delete": { + } + }, + "/api/v1/status-pages/{id}/incidents/{incidentId}/publish": { + "post": { "tags": [ - "Service Subscriptions" + "Status Pages" ], - "summary": "Remove a subscription by its ID", - "description": "Removes a specific subscription (whole-service or component-level). No-op if not found.", - "operationId": "unsubscribe_1", + "summary": "Publish a draft incident (sets publishedAt, applies component statuses, notifies subscribers)", + "operationId": "publishIncident", "parameters": [ { "name": "id", @@ -3888,26 +17667,9 @@ "type": "string", "format": "uuid" } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/service-subscriptions/{id}/alert-sensitivity": { - "patch": { - "tags": [ - "Service Subscriptions" - ], - "summary": "Update alert sensitivity for a subscription", - "description": "Controls which external incidents trigger alerts: ALL (any status change), INCIDENTS_ONLY (real vendor incidents, default), MAJOR_ONLY (only DOWN-level incidents).", - "operationId": "updateAlertSensitivity", - "parameters": [ + }, { - "name": "id", + "name": "incidentId", "in": "path", "required": true, "schema": { @@ -3920,11 +17682,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateAlertSensitivityRequest" + "$ref": "#/components/schemas/PublishStatusPageIncidentRequest" } } - }, - "required": true + } }, "responses": { "200": { @@ -3932,7 +17693,87 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseServiceSubscriptionDto" + "$ref": "#/components/schemas/SingleValueResponseStatusPageIncidentDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -3940,21 +17781,30 @@ } } }, - "/api/v1/service-subscriptions/{slug}": { + "/api/v1/status-pages/{id}/incidents/{incidentId}/updates": { "post": { "tags": [ - "Service Subscriptions" + "Status Pages" ], - "summary": "Subscribe to a service or a component of a service", - "description": "Idempotent — returns the existing subscription if an identical one exists. Omit the request body or set componentId to null for a whole-service subscription. Free tier: max 10 subscriptions. Paid tier: unlimited.", - "operationId": "subscribe_1", + "summary": "Post an incident timeline update", + "operationId": "postIncidentUpdate", "parameters": [ { - "name": "slug", + "name": "id", "in": "path", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" + } + }, + { + "name": "incidentId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } } ], @@ -3962,10 +17812,11 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServiceSubscribeRequest" + "$ref": "#/components/schemas/CreateStatusPageIncidentUpdateRequest" } } - } + }, + "required": true }, "responses": { "201": { @@ -3973,108 +17824,87 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseServiceSubscriptionDto" + "$ref": "#/components/schemas/SingleValueResponseStatusPageIncidentDto" } } } - } - } - } - }, - "/api/v1/services": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "List all enabled services (cursor-paginated)", - "operationId": "listServices", - "parameters": [ - { - "name": "category", - "in": "query", - "description": "Filter by category (exact match)", - "required": false, - "schema": { - "type": "string" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "status", - "in": "query", - "description": "Filter by current overall_status (exact match)", - "required": false, - "schema": { - "type": "string" + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "published", - "in": "query", - "description": "Filter by published status for pSEO pages", - "required": false, - "schema": { - "type": "boolean" + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "cursor", - "in": "query", - "description": "Opaque cursor from a previous response", - "required": false, - "schema": { - "type": "string" + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "limit", - "in": "query", - "description": "Page size (1–100, default 20)", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 20 + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "500": { + "description": "Internal server error — see the message field for details", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/CursorPageServiceCatalogDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/services/{slugOrId}": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "Get a single service by slug or UUID with current status, components, and recent incidents", - "operationId": "getService", - "parameters": [ - { - "name": "slugOrId", - "in": "path", - "required": true, - "schema": { - "type": "string" + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "503": { + "description": "Service unavailable — try again shortly", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseServiceDetailDto" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -4082,132 +17912,114 @@ } } }, - "/api/v1/services/{slugOrId}/components": { - "get": { + "/api/v1/status-pages/{id}/layout/reorder": { + "put": { "tags": [ - "Status Data" + "Status Pages" ], - "summary": "List active components for a service with current status and inline uptime", - "operationId": "getComponents", + "summary": "Reorder page-level layout: groups and ungrouped components share one ordering", + "operationId": "reorderLayout", "parameters": [ { - "name": "slugOrId", + "name": "id", "in": "path", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReorderPageLayoutRequest" + } + } + }, + "required": true + }, "responses": { - "200": { - "description": "OK", + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultServiceComponentDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/services/{slugOrId}/components/{componentId}/uptime": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "Get daily uptime data for a component", - "operationId": "getComponentUptime", - "parameters": [ - { - "name": "slugOrId", - "in": "path", - "required": true, - "schema": { - "type": "string" + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "componentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "period", - "in": "query", - "description": "Time window", - "required": false, - "schema": { - "type": "string", - "enum": [ - "7d", - "30d", - "90d", - "1y" - ] + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "409": { + "description": "Conflict — the request collides with current resource state", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultComponentUptimeDayDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/services/{slugOrId}/components/uptime": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "Batch daily uptime data for all leaf components", - "description": "Returns daily uptime for every active non-group component with uptime data, keyed by component ID. Replaces N individual per-component uptime calls with a single request.", - "operationId": "getBatchComponentUptime", - "parameters": [ - { - "name": "slugOrId", - "in": "path", - "required": true, - "schema": { - "type": "string" + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "period", - "in": "query", - "description": "Time window", - "required": false, - "schema": { - "type": "string", - "enum": [ - "7d", - "30d", - "90d", - "1y" - ] + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "503": { + "description": "Service unavailable — try again shortly", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseMapStringListComponentUptimeDayDto" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -4215,31 +18027,29 @@ } } }, - "/api/v1/services/{slugOrId}/days/{date}": { + "/api/v1/status-pages/{id}/subscribers": { "get": { "tags": [ - "Status Data" + "Status Pages" ], - "summary": "One-day rollup for a service: aggregated uptime, per-component impacts, and overlapping incidents", - "description": "Powers the click/hover-to-expand panel under each uptime bar on the public status page. Single round-trip — components, sums, and overlapping incidents (with affected component names) are returned in one response.", - "operationId": "getServiceDayDetail", + "summary": "List confirmed subscribers (paginated)", + "operationId": "listSubscribers", "parameters": [ { - "name": "slugOrId", + "name": "id", "in": "path", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } }, { - "name": "date", - "in": "path", - "description": "UTC calendar day in ISO format (YYYY-MM-DD)", + "name": "pageable", + "in": "query", "required": true, "schema": { - "type": "string", - "format": "date" + "$ref": "#/components/schemas/Pageable" } } ], @@ -4249,94 +18059,102 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseServiceDayDetailDto" + "$ref": "#/components/schemas/TableValueResultStatusPageSubscriberDto" } } } - } - } - } - }, - "/api/v1/services/{slugOrId}/incidents": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "List incident history for a service (paginated)", - "operationId": "listIncidents_1", - "parameters": [ - { - "name": "slugOrId", - "in": "path", - "required": true, - "schema": { - "type": "string" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "from", - "in": "query", - "description": "Earliest start date (ISO 8601 date)", - "required": false, - "schema": { - "type": "string", - "format": "date" + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "status", - "in": "query", - "description": "Filter: active (unresolved), resolved, or omit for all", - "required": false, - "schema": { - "type": "string", - "enum": [ - "active", - "resolved" - ] + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "503": { + "description": "Service unavailable — try again shortly", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultServiceIncidentDto" + "$ref": "#/components/schemas/ErrorResponse" } } } } } - } - }, - "/api/v1/services/{slugOrId}/incidents/{incidentId}": { - "get": { + }, + "post": { "tags": [ - "Status Data" + "Status Pages" ], - "summary": "Get incident detail with full update timeline", - "operationId": "getIncident_1", + "summary": "Add a subscriber (immediately confirmed, skips double opt-in)", + "operationId": "addSubscriber", "parameters": [ { - "name": "slugOrId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "incidentId", + "name": "id", "in": "path", "required": true, "schema": { @@ -4345,139 +18163,103 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminAddSubscriberRequest" + } + } + }, + "required": true + }, "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseServiceIncidentDetailDto" + "$ref": "#/components/schemas/SingleValueResponseStatusPageSubscriberDto" } } } - } - } - } - }, - "/api/v1/services/{slugOrId}/live-status": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "Lightweight live-status snapshot for polling", - "description": "Returns only the current overall status, component statuses, and active incident count. Designed for frequent polling with minimal payload.", - "operationId": "getServiceLiveStatus", - "parameters": [ - { - "name": "slugOrId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "400": { + "description": "Bad request — the payload failed validation", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseServiceLiveStatusDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/services/{slugOrId}/maintenances": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "List scheduled maintenances for a service", - "operationId": "getScheduledMaintenances", - "parameters": [ - { - "name": "slugOrId", - "in": "path", - "required": true, - "schema": { - "type": "string" + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "status", - "in": "query", - "description": "Filter by status (e.g. scheduled, in_progress, verifying, completed)", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string" + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "404": { + "description": "Not found — the requested resource does not exist", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultScheduledMaintenanceDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/services/{slugOrId}/poll-results": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "List poll results for a service (cursor-paginated)", - "operationId": "listPollResults", - "parameters": [ - { - "name": "slugOrId", - "in": "path", - "required": true, - "schema": { - "type": "string" + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "cursor", - "in": "query", - "description": "ISO 8601 timestamp cursor from a previous response", - "required": false, - "schema": { - "type": "string" + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "limit", - "in": "query", - "description": "Page size (1–100, default 50)", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 50 + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "503": { + "description": "Service unavailable — try again shortly", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/CursorPageServicePollResultDto" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -4485,44 +18267,113 @@ } } }, - "/api/v1/services/{slugOrId}/poll-summary": { - "get": { + "/api/v1/status-pages/{id}/subscribers/{subscriberId}": { + "delete": { "tags": [ - "Status Data" + "Status Pages" ], - "summary": "Get aggregated poll metrics and chart data for a service", - "operationId": "getPollSummary", + "summary": "Remove a subscriber", + "operationId": "removeSubscriber", "parameters": [ { - "name": "slugOrId", + "name": "id", "in": "path", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } }, { - "name": "window", - "in": "query", - "description": "Time window", - "required": false, + "name": "subscriberId", + "in": "path", + "required": true, "schema": { "type": "string", - "enum": [ - "24h", - "7d", - "30d" - ] + "format": "uuid" } } ], "responses": { - "200": { - "description": "OK", + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", "content": { - "*/*": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseServicePollSummaryDto" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -4530,53 +18381,20 @@ } } }, - "/api/v1/services/{slugOrId}/uptime": { + "/api/v1/tags": { "get": { "tags": [ - "Status Data" + "Tags" ], - "summary": "Get uptime statistics for a service", - "description": "Uptime data aggregated across active non-group components.", - "operationId": "getServiceUptime", + "summary": "List tags for the authenticated organization", + "operationId": "list_3", "parameters": [ { - "name": "slugOrId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "period", - "in": "query", - "description": "Time window", - "required": false, - "schema": { - "type": "string", - "enum": [ - "24h", - "7d", - "30d", - "90d", - "1y", - "2y", - "all" - ] - } - }, - { - "name": "granularity", + "name": "pageable", "in": "query", - "description": "Bucket granularity", - "required": false, + "required": true, "schema": { - "type": "string", - "enum": [ - "hourly", - "daily", - "monthly" - ] + "$ref": "#/components/schemas/Pageable" } } ], @@ -4586,119 +18404,87 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseServiceUptimeResponse" + "$ref": "#/components/schemas/TableValueResultTagDto" } } } - } - }, - "security": [ - { - "BearerAuth": [] - } - ] - } - }, - "/api/v1/services/incidents": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "List vendor incidents across all services (paginated)", - "description": "Cross-service vendor incident feed ordered by start date descending.", - "operationId": "listCrossServiceIncidents", - "parameters": [ - { - "name": "from", - "in": "query", - "description": "Earliest start date (ISO 8601 date)", - "required": false, - "schema": { - "type": "string", - "format": "date" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "status", - "in": "query", - "description": "Filter: active (unresolved), resolved, or omit for all", - "required": false, - "schema": { - "type": "string", - "enum": [ - "active", - "resolved" - ] + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "category", - "in": "query", - "description": "Filter by service category", - "required": false, - "schema": { - "type": "string" + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "409": { + "description": "Conflict — the request collides with current resource state", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultServiceIncidentDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/services/summary": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "Global status summary across all services", - "description": "Returns aggregate counts of services by status and a list of services currently experiencing issues.", - "operationId": "getGlobalStatusSummary", - "responses": { - "200": { - "description": "OK", + }, + "500": { + "description": "Internal server error — see the message field for details", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseGlobalStatusSummaryDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/status-pages": { - "get": { - "tags": [ - "Status Pages" - ], - "summary": "List status pages for the workspace", - "operationId": "list_4", - "responses": { - "200": { - "description": "OK", + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultStatusPageDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -4707,15 +18493,15 @@ }, "post": { "tags": [ - "Status Pages" + "Tags" ], - "summary": "Create a status page", - "operationId": "create_5", + "summary": "Create a new tag", + "operationId": "create_4", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateStatusPageRequest" + "$ref": "#/components/schemas/CreateTagRequest" } } }, @@ -4727,116 +18513,101 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseStatusPageDto" + "$ref": "#/components/schemas/SingleValueResponseTagDto" } } } - } - } - } - }, - "/api/v1/status-pages/{id}": { - "get": { - "tags": [ - "Status Pages" - ], - "summary": "Get a status page", - "operationId": "get_2", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseStatusPageDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "put": { - "tags": [ - "Status Pages" - ], - "summary": "Update a status page", - "operationId": "update_3", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStatusPageRequest" + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "502": { + "description": "Bad gateway — an upstream provider returned an error", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseStatusPageDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "delete": { - "tags": [ - "Status Pages" - ], - "summary": "Delete a status page", - "operationId": "delete_3", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } } - ], - "responses": { - "204": { - "description": "No Content" - } } } }, - "/api/v1/status-pages/{id}/components": { + "/api/v1/tags/{id}": { "get": { "tags": [ - "Status Pages" + "Tags" ], - "summary": "List all components", - "operationId": "listComponents", + "summary": "Get a tag by ID", + "operationId": "getById", "parameters": [ { "name": "id", @@ -4854,61 +18625,99 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/TableValueResultStatusPageComponentDto" + "$ref": "#/components/schemas/SingleValueResponseTagDto" } } } - } - } - }, - "post": { - "tags": [ - "Status Pages" - ], - "summary": "Add a component to the status page", - "operationId": "createComponent", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateStatusPageComponentRequest" + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "403": { + "description": "Forbidden — the actor lacks permission for this resource", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseStatusPageComponentDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } } } - } - }, - "/api/v1/status-pages/{id}/components/{componentId}": { + }, "put": { "tags": [ - "Status Pages" + "Tags" ], - "summary": "Update a component", - "operationId": "updateComponent", + "summary": "Update a tag's name and/or color", + "operationId": "update_2", "parameters": [ { "name": "id", @@ -4918,22 +18727,13 @@ "type": "string", "format": "uuid" } - }, - { - "name": "componentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } } ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateStatusPageComponentRequest" + "$ref": "#/components/schemas/UpdateTagRequest" } } }, @@ -4945,104 +18745,99 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseStatusPageComponentDto" + "$ref": "#/components/schemas/SingleValueResponseTagDto" } } } - } - } - }, - "delete": { - "tags": [ - "Status Pages" - ], - "summary": "Remove a component from the status page", - "operationId": "deleteComponent", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "componentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/status-pages/{id}/components/{componentId}/uptime": { - "get": { - "tags": [ - "Status Pages" - ], - "summary": "Get component uptime history (daily rollups)", - "operationId": "componentUptime_1", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "componentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "days", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 90 + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "500": { + "description": "Internal server error — see the message field for details", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultComponentUptimeDayDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } } - } - } - }, - "/api/v1/status-pages/{id}/components/reorder": { - "put": { + } + }, + "delete": { "tags": [ - "Status Pages" + "Tags" ], - "summary": "Batch reorder components (and optionally move between groups)", - "operationId": "reorderComponents", + "summary": "Delete a tag (cascades to all monitor associations)", + "operationId": "delete_2", "parameters": [ { "name": "id", @@ -5054,88 +18849,188 @@ } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReorderComponentsRequest" - } - } - }, - "required": true - }, "responses": { "204": { "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } }, - "/api/v1/status-pages/{id}/domains": { - "get": { + "/api/v1/vaults/rotate": { + "post": { "tags": [ - "Status Pages" - ], - "summary": "List custom domains", - "operationId": "listDomains", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } + "Vault" ], + "summary": "Rotate DEK", + "description": "Generates a new Data Encryption Key, re-encrypts all secrets and alert-channel configs, and bumps the vault version. Admin-only. Pipeline DEK caches expire within ~10 minutes.", + "operationId": "rotateDek", "responses": { "200": { "description": "OK", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/TableValueResultStatusPageCustomDomainDto" + "$ref": "#/components/schemas/SingleValueResponseDekRotationResultDto" } } } - } - } - }, - "post": { - "tags": [ - "Status Pages" - ], - "summary": "Add a custom domain", - "operationId": "addDomain", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AddCustomDomainRequest" + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "403": { + "description": "Forbidden — the actor lacks permission for this resource", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseStatusPageCustomDomainDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -5143,106 +19038,110 @@ } } }, - "/api/v1/status-pages/{id}/domains/{domainId}": { - "delete": { + "/api/v1/webhooks": { + "get": { "tags": [ - "Status Pages" + "Webhooks" ], - "summary": "Remove a custom domain", - "operationId": "removeDomain", + "summary": "List webhook endpoints for the authenticated org", + "operationId": "list_2", "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "domainId", - "in": "path", + "name": "pageable", + "in": "query", "required": true, "schema": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/Pageable" } } ], "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/status-pages/{id}/domains/{domainId}/verify": { - "post": { - "tags": [ - "Status Pages" - ], - "summary": "Trigger domain verification check", - "operationId": "verifyDomain", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultWebhookEndpointDto" + } + } } }, - { - "name": "domainId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "500": { + "description": "Internal server error — see the message field for details", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseStatusPageCustomDomainDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/status-pages/{id}/groups": { - "get": { - "tags": [ - "Status Pages" - ], - "summary": "List component groups with nested components", - "operationId": "listGroups", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "503": { + "description": "Service unavailable — try again shortly", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultStatusPageComponentGroupDto" + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -5251,26 +19150,15 @@ }, "post": { "tags": [ - "Status Pages" - ], - "summary": "Create a component group", - "operationId": "createGroup", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } + "Webhooks" ], + "summary": "Register a new webhook endpoint", + "operationId": "create_3", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateStatusPageComponentGroupRequest" + "$ref": "#/components/schemas/CreateWebhookEndpointRequest" } } }, @@ -5282,104 +19170,101 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseStatusPageComponentGroupDto" + "$ref": "#/components/schemas/SingleValueResponseWebhookEndpointDto" } } } - } - } - } - }, - "/api/v1/status-pages/{id}/groups/{groupId}": { - "put": { - "tags": [ - "Status Pages" - ], - "summary": "Update a component group", - "operationId": "updateGroup", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "groupId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStatusPageComponentGroupRequest" + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "404": { + "description": "Not found — the requested resource does not exist", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseStatusPageComponentGroupDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "delete": { - "tags": [ - "Status Pages" - ], - "summary": "Delete a component group (components become ungrouped)", - "operationId": "deleteGroup", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "groupId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "204": { - "description": "No Content" } } } }, - "/api/v1/status-pages/{id}/incidents": { + "/api/v1/webhooks/{id}": { "get": { "tags": [ - "Status Pages" + "Webhooks" ], - "summary": "List incidents for this status page (filterable by status, paginated)", - "operationId": "listIncidents", + "summary": "Get a single webhook endpoint", + "operationId": "get_1", "parameters": [ { "name": "id", @@ -5389,31 +19274,6 @@ "type": "string", "format": "uuid" } - }, - { - "name": "status", - "in": "query", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "INVESTIGATING", - "IDENTIFIED", - "MONITORING", - "RESOLVED" - ] - } - } - }, - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" - } } ], "responses": { @@ -5422,19 +19282,99 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/TableValueResultStatusPageIncidentDto" + "$ref": "#/components/schemas/SingleValueResponseWebhookEndpointDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } } } }, - "post": { + "put": { "tags": [ - "Status Pages" + "Webhooks" ], - "summary": "Create a status page incident (manual)", - "operationId": "createIncident", + "summary": "Update a webhook endpoint", + "operationId": "update_1", "parameters": [ { "name": "id", @@ -5450,109 +19390,99 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateStatusPageIncidentRequest" + "$ref": "#/components/schemas/UpdateWebhookEndpointRequest" } } }, "required": true }, "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseStatusPageIncidentDto" + "$ref": "#/components/schemas/SingleValueResponseWebhookEndpointDto" } } } - } - } - } - }, - "/api/v1/status-pages/{id}/incidents/{incidentId}": { - "get": { - "tags": [ - "Status Pages" - ], - "summary": "Get incident details with timeline", - "operationId": "getIncident", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "incidentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseStatusPageIncidentDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "put": { - "tags": [ - "Status Pages" - ], - "summary": "Update an incident", - "operationId": "updateIncident", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "incidentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStatusPageIncidentRequest" + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "502": { + "description": "Bad gateway — an upstream provider returned an error", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseStatusPageIncidentDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -5561,10 +19491,10 @@ }, "delete": { "tags": [ - "Status Pages" + "Webhooks" ], - "summary": "Delete an incident", - "operationId": "deleteIncident", + "summary": "Delete a webhook endpoint", + "operationId": "delete_1", "parameters": [ { "name": "id", @@ -5574,201 +19504,102 @@ "type": "string", "format": "uuid" } - }, - { - "name": "incidentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } } ], "responses": { "204": { "description": "No Content" - } - } - } - }, - "/api/v1/status-pages/{id}/incidents/{incidentId}/dismiss": { - "post": { - "tags": [ - "Status Pages" - ], - "summary": "Dismiss a draft incident (deletes it permanently)", - "operationId": "dismissIncident", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } }, - { - "name": "incidentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/status-pages/{id}/incidents/{incidentId}/publish": { - "post": { - "tags": [ - "Status Pages" - ], - "summary": "Publish a draft incident (sets publishedAt, applies component statuses, notifies subscribers)", - "operationId": "publishIncident", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "incidentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublishStatusPageIncidentRequest" + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } - } - }, - "responses": { - "200": { - "description": "OK", + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseStatusPageIncidentDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/status-pages/{id}/incidents/{incidentId}/updates": { - "post": { - "tags": [ - "Status Pages" - ], - "summary": "Post an incident timeline update", - "operationId": "postIncidentUpdate", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } }, - { - "name": "incidentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateStatusPageIncidentUpdateRequest" + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "409": { + "description": "Conflict — the request collides with current resource state", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseStatusPageIncidentDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/status-pages/{id}/layout/reorder": { - "put": { - "tags": [ - "Status Pages" - ], - "summary": "Reorder page-level layout: groups and ungrouped components share one ordering", - "operationId": "reorderLayout", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReorderPageLayoutRequest" + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "204": { - "description": "No Content" + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } }, - "/api/v1/status-pages/{id}/subscribers": { + "/api/v1/webhooks/{id}/deliveries": { "get": { "tags": [ - "Status Pages" + "Webhooks" ], - "summary": "List confirmed subscribers (paginated)", - "operationId": "listSubscribers", + "summary": "List recent deliveries for a webhook endpoint", + "operationId": "listDeliveries", "parameters": [ { "name": "id", @@ -5780,11 +19611,13 @@ } }, { - "name": "pageable", + "name": "limit", "in": "query", - "required": true, + "required": false, "schema": { - "$ref": "#/components/schemas/Pageable" + "type": "integer", + "format": "int32", + "default": 20 } } ], @@ -5794,19 +19627,101 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/TableValueResultStatusPageSubscriberDto" + "$ref": "#/components/schemas/TableValueResultWebhookDeliveryDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } } } - }, + } + }, + "/api/v1/webhooks/{id}/test": { "post": { "tags": [ - "Status Pages" + "Webhooks" ], - "summary": "Add a subscriber (immediately confirmed, skips double opt-in)", - "operationId": "addSubscriber", + "summary": "Send a test delivery to a webhook endpoint", + "operationId": "test", "parameters": [ { "name": "id", @@ -5822,19 +19737,98 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminAddSubscriberRequest" + "$ref": "#/components/schemas/TestWebhookEndpointRequest" } } - }, - "required": true + } }, "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseStatusPageSubscriberDto" + "$ref": "#/components/schemas/SingleValueResponseWebhookTestResult" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -5842,270 +19836,303 @@ } } }, - "/api/v1/status-pages/{id}/subscribers/{subscriberId}": { - "delete": { + "/api/v1/webhooks/events": { + "get": { "tags": [ - "Status Pages" + "Webhooks" ], - "summary": "Remove a subscriber", - "operationId": "removeSubscriber", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "summary": "List all available webhook event types", + "description": "Returns the full catalog of supported outbound webhook event types with their surface grouping and human-readable descriptions. Use this to populate subscription checkboxes when creating or updating a webhook endpoint.", + "operationId": "listEvents", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/WebhookEventCatalogResponse" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "subscriberId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "204": { - "description": "No Content" } } } }, - "/api/v1/tags": { + "/api/v1/webhooks/signing-secret": { "get": { "tags": [ - "Tags" - ], - "summary": "List tags for the authenticated organization", - "operationId": "list_3", - "parameters": [ - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" - } - } + "Webhooks" ], + "summary": "Get signing secret metadata for the authenticated org", + "operationId": "getSigningSecretInfo", "responses": { "200": { "description": "OK", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/TableValueResultTagDto" + "$ref": "#/components/schemas/SingleValueResponseWebhookSigningSecretDto" } } } - } - } - }, - "post": { - "tags": [ - "Tags" - ], - "summary": "Create a new tag", - "operationId": "create_4", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateTagRequest" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "401": { + "description": "Unauthorized — missing or invalid credentials", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseTagDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/tags/{id}": { - "get": { - "tags": [ - "Tags" - ], - "summary": "Get a tag by ID", - "operationId": "getById", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "404": { + "description": "Not found — the requested resource does not exist", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseTagDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "put": { - "tags": [ - "Tags" - ], - "summary": "Update a tag's name and/or color", - "operationId": "update_2", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateTagRequest" + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "502": { + "description": "Bad gateway — an upstream provider returned an error", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseTagDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "delete": { - "tags": [ - "Tags" - ], - "summary": "Delete a tag (cascades to all monitor associations)", - "operationId": "delete_2", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } } - ], - "responses": { - "204": { - "description": "No Content" - } } } }, - "/api/v1/vaults/rotate": { + "/api/v1/webhooks/signing-secret/rotate": { "post": { "tags": [ - "Vault" + "Webhooks" ], - "summary": "Rotate DEK", - "description": "Generates a new Data Encryption Key, re-encrypts all secrets and alert-channel configs, and bumps the vault version. Admin-only. Pipeline DEK caches expire within ~10 minutes.", - "operationId": "rotateDek", + "summary": "Generate or rotate the organization webhook signing secret", + "operationId": "rotateSigningSecret", "responses": { "200": { "description": "OK", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseDekRotationResultDto" + "$ref": "#/components/schemas/SingleValueResponseString" } } } - } - } - } - }, - "/api/v1/webhooks": { - "get": { - "tags": [ - "Webhooks" - ], - "summary": "List webhook endpoints for the authenticated org", - "operationId": "list_2", - "parameters": [ - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultWebhookEndpointDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "post": { - "tags": [ - "Webhooks" - ], - "summary": "Register a new webhook endpoint", - "operationId": "create_3", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateWebhookEndpointRequest" + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "502": { + "description": "Bad gateway — an upstream provider returned an error", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseWebhookEndpointDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -6113,21 +20140,20 @@ } } }, - "/api/v1/webhooks/{id}": { + "/api/v1/workspaces": { "get": { "tags": [ - "Webhooks" + "Workspaces" ], - "summary": "Get a single webhook endpoint", - "operationId": "get_1", + "summary": "List workspaces", + "operationId": "list_1", "parameters": [ { - "name": "id", - "in": "path", + "name": "pageable", + "in": "query", "required": true, "schema": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/Pageable" } } ], @@ -6137,277 +20163,196 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseWebhookEndpointDto" + "$ref": "#/components/schemas/TableValueResultWorkspaceDto" } } } - } - } - }, - "put": { - "tags": [ - "Webhooks" - ], - "summary": "Update a webhook endpoint", - "operationId": "update_1", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateWebhookEndpointRequest" + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "200": { - "description": "OK", + "403": { + "description": "Forbidden — the actor lacks permission for this resource", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseWebhookEndpointDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "delete": { - "tags": [ - "Webhooks" - ], - "summary": "Delete a webhook endpoint", - "operationId": "delete_1", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/webhooks/{id}/deliveries": { - "get": { - "tags": [ - "Webhooks" - ], - "summary": "List recent deliveries for a webhook endpoint", - "operationId": "listDeliveries", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 20 + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultWebhookDeliveryDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } } } - } - }, - "/api/v1/webhooks/{id}/test": { + }, "post": { "tags": [ - "Webhooks" - ], - "summary": "Send a test delivery to a webhook endpoint", - "operationId": "test", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } + "Workspaces" ], + "summary": "Create workspace", + "operationId": "create_2", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestWebhookEndpointRequest" + "$ref": "#/components/schemas/CreateWorkspaceRequest" } } - } + }, + "required": true }, "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseWebhookTestResult" + "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" } } } - } - } - } - }, - "/api/v1/webhooks/events": { - "get": { - "tags": [ - "Webhooks" - ], - "summary": "List all available webhook event types", - "description": "Returns the full catalog of supported outbound webhook event types with their surface grouping and human-readable descriptions. Use this to populate subscription checkboxes when creating or updating a webhook endpoint.", - "operationId": "listEvents", - "responses": { - "200": { - "description": "OK", + }, + "400": { + "description": "Bad request — the payload failed validation", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/WebhookEventCatalogResponse" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/webhooks/signing-secret": { - "get": { - "tags": [ - "Webhooks" - ], - "summary": "Get signing secret metadata for the authenticated org", - "operationId": "getSigningSecretInfo", - "responses": { - "200": { - "description": "OK", + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseWebhookSigningSecretDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/v1/webhooks/signing-secret/rotate": { - "post": { - "tags": [ - "Webhooks" - ], - "summary": "Generate or rotate the organization webhook signing secret", - "operationId": "rotateSigningSecret", - "responses": { - "200": { - "description": "OK", + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseString" + "$ref": "#/components/schemas/ErrorResponse" } - } - } - } - } - } - }, - "/api/v1/workspaces": { - "get": { - "tags": [ - "Workspaces" - ], - "summary": "List workspaces", - "operationId": "list_1", - "parameters": [ - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "404": { + "description": "Not found — the requested resource does not exist", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/TableValueResultWorkspaceDto" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - }, - "post": { - "tags": [ - "Workspaces" - ], - "summary": "Create workspace", - "operationId": "create_2", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateWorkspaceRequest" + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "required": true - }, - "responses": { - "201": { - "description": "Created", + "500": { + "description": "Internal server error — see the message field for details", "content": { - "*/*": { + "application/json": { "schema": { - "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } @@ -6443,6 +20388,86 @@ } } } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } }, @@ -6483,6 +20508,86 @@ } } } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } }, @@ -6506,6 +20611,86 @@ "responses": { "204": { "description": "No Content" + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } @@ -6780,7 +20965,10 @@ "eventType", "id", "incidentId", - "status" + "status", + "stepNumber", + "fireCount", + "attemptCount" ], "type": "object", "properties": { @@ -6912,7 +21100,8 @@ "required": [ "createdAt", "key", - "name" + "name", + "id" ], "type": "object", "properties": { @@ -6950,7 +21139,8 @@ "createdAt", "key", "name", - "updatedAt" + "updatedAt", + "id" ], "type": "object", "properties": { @@ -7062,7 +21252,8 @@ "AssertionResultDto": { "required": [ "severity", - "type" + "type", + "passed" ], "type": "object", "properties": { @@ -7107,7 +21298,8 @@ "required": [ "assertionType", "message", - "severity" + "severity", + "passed" ], "type": "object", "properties": { @@ -7190,7 +21382,8 @@ "AuditEventDto": { "required": [ "action", - "createdAt" + "createdAt", + "id" ], "type": "object", "properties": { @@ -7289,6 +21482,26 @@ } ] }, + "BatchComponentUptimeDto": { + "required": [ + "components" + ], + "type": "object", + "properties": { + "components": { + "type": "object", + "additionalProperties": { + "type": "array", + "description": "Map of component ID → list of per-day uptime entries (oldest → newest)", + "items": { + "$ref": "#/components/schemas/ComponentUptimeDayDto" + } + }, + "description": "Map of component ID → list of per-day uptime entries (oldest → newest)" + } + }, + "description": "Batch daily uptime per component, keyed by component ID" + }, "BearerAuthConfig": { "type": "object", "allOf": [ @@ -7352,7 +21565,7 @@ }, "action": { "type": "string", - "description": "Action to perform: PAUSE, RESUME, DELETE, ADD_TAG, REMOVE_TAG", + "description": "Action to apply to every monitor in the bulk request", "enum": [ "PAUSE", "RESUME", @@ -7410,7 +21623,8 @@ }, "CategoryDto": { "required": [ - "category" + "category", + "serviceCount" ], "type": "object", "properties": { @@ -7612,7 +21826,8 @@ "required": [ "id", "region", - "timestamp" + "timestamp", + "passed" ], "type": "object", "properties": { @@ -7682,13 +21897,23 @@ }, "description": "Check-type-specific details — polymorphic by check_type discriminator", "discriminator": { - "propertyName": "check_type" + "propertyName": "check_type", + "mapping": { + "http": "#/components/schemas/Http", + "tcp": "#/components/schemas/Tcp", + "icmp": "#/components/schemas/Icmp", + "dns": "#/components/schemas/Dns", + "mcp_server": "#/components/schemas/McpServer" + } } }, "ComponentImpact": { "required": [ "componentId", - "componentName" + "componentName", + "uptimePercentage", + "partialOutageSeconds", + "majorOutageSeconds" ], "type": "object", "properties": { @@ -7726,7 +21951,8 @@ }, "ComponentPosition": { "required": [ - "componentId" + "componentId", + "displayOrder" ], "type": "object", "properties": { @@ -7774,40 +22000,51 @@ }, "ComponentUptimeDayDto": { "required": [ - "date" + "date", + "source", + "partialOutageSeconds", + "majorOutageSeconds", + "degradedSeconds", + "uptimePercentage" ], "type": "object", "properties": { "date": { "type": "string", - "description": "Start-of-day timestamp for this bucket (UTC midnight)", + "description": "Date of the daily bucket (ISO 8601)", "format": "date-time" }, "partialOutageSeconds": { "type": "integer", - "description": "Seconds of partial outage on this day", + "description": "Seconds of partial outage observed on this day", "format": "int32" }, "majorOutageSeconds": { "type": "integer", - "description": "Seconds of major outage on this day", + "description": "Seconds of major outage observed on this day", + "format": "int32" + }, + "degradedSeconds": { + "type": "integer", + "description": "Seconds the component spent in degraded performance on this day", "format": "int32" }, "uptimePercentage": { "type": "number", - "description": "Computed uptime percentage using weighted formula", + "description": "Computed uptime percentage for the day", "format": "double" }, - "incidents": { - "type": "array", - "description": "Incidents that overlapped this day", - "nullable": true, - "items": { - "$ref": "#/components/schemas/IncidentRef" - } + "eventsJson": { + "type": "string", + "description": "Incident event references for this day as raw JSON", + "nullable": true + }, + "source": { + "type": "string", + "description": "Data source: vendor_reported or incident_derived" } }, - "description": "Daily uptime data for a status page component" + "description": "Daily uptime data for a component" }, "ComponentUptimeSummaryDto": { "required": [ @@ -7846,7 +22083,9 @@ }, "ConfirmationPolicy": { "required": [ - "type" + "type", + "minRegionsFailing", + "maxWaitSeconds" ], "type": "object", "properties": { @@ -7932,8 +22171,7 @@ }, "CreateAssertionRequest": { "required": [ - "config", - "severity" + "config" ], "type": "object", "properties": { @@ -8069,7 +22307,8 @@ }, "severity": { "type": "string", - "description": "Outcome severity: FAIL (fails the check) or WARN (warns without failing)", + "description": "Outcome severity: FAIL (fails the check) or WARN (warns without failing, default: FAIL)", + "nullable": true, "enum": [ "fail", "warn" @@ -8081,7 +22320,8 @@ "CreateEnvironmentRequest": { "required": [ "name", - "slug" + "slug", + "isDefault" ], "type": "object", "properties": { @@ -8243,26 +22483,7 @@ ] }, "config": { - "oneOf": [ - { - "$ref": "#/components/schemas/DnsMonitorConfig" - }, - { - "$ref": "#/components/schemas/HeartbeatMonitorConfig" - }, - { - "$ref": "#/components/schemas/HttpMonitorConfig" - }, - { - "$ref": "#/components/schemas/IcmpMonitorConfig" - }, - { - "$ref": "#/components/schemas/McpServerMonitorConfig" - }, - { - "$ref": "#/components/schemas/TcpMonitorConfig" - } - ] + "$ref": "#/components/schemas/MonitorConfig" }, "frequencySeconds": { "type": "integer", @@ -8345,11 +22566,8 @@ }, "CreateNotificationPolicyRequest": { "required": [ - "enabled", "escalation", - "matchRules", - "name", - "priority" + "name" ], "type": "object", "properties": { @@ -8362,6 +22580,7 @@ "matchRules": { "type": "array", "description": "Match rules to evaluate (all must pass; omit or empty for catch-all)", + "nullable": true, "items": { "$ref": "#/components/schemas/MatchRule" } @@ -8372,12 +22591,14 @@ "enabled": { "type": "boolean", "description": "Whether this policy is enabled (default true)", + "nullable": true, "default": true }, "priority": { "type": "integer", "description": "Evaluation priority; higher value = evaluated first (default 0)", "format": "int32", + "nullable": true, "default": 0 } }, @@ -8856,35 +23077,10 @@ }, "description": "Create a new workspace within the organization" }, - "CursorPage": { - "required": [ - "data" - ], - "type": "object", - "properties": { - "data": { - "type": "array", - "description": "Items on this page", - "items": { - "type": "object", - "description": "Items on this page" - } - }, - "nextCursor": { - "type": "string", - "description": "Opaque cursor for the next page; null when there are no more results", - "nullable": true - }, - "hasMore": { - "type": "boolean", - "description": "Whether more results exist beyond this page" - } - }, - "description": "Cursor-paginated response for time-series and append-only data" - }, "CursorPageCheckResultDto": { "required": [ - "data" + "data", + "hasMore" ], "type": "object", "properties": { @@ -8909,7 +23105,8 @@ }, "CursorPageServiceCatalogDto": { "required": [ - "data" + "data", + "hasMore" ], "type": "object", "properties": { @@ -8934,7 +23131,8 @@ }, "CursorPageServicePollResultDto": { "required": [ - "data" + "data", + "hasMore" ], "type": "object", "properties": { @@ -8979,7 +23177,8 @@ "id", "impact", "status", - "title" + "title", + "scheduled" ], "type": "object", "properties": { @@ -9041,7 +23240,11 @@ }, "DekRotationResultDto": { "required": [ - "rotatedAt" + "rotatedAt", + "previousDekVersion", + "newDekVersion", + "secretsReEncrypted", + "channelsReEncrypted" ], "type": "object", "properties": { @@ -9098,7 +23301,8 @@ "attemptedAt", "deliveryId", "id", - "status" + "status", + "attemptNumber" ], "type": "object", "properties": { @@ -9229,6 +23433,80 @@ } ] }, + "Dns": { + "type": "object", + "description": "DNS check-type-specific details", + "allOf": [ + { + "$ref": "#/components/schemas/CheckTypeDetailsDto" + }, + { + "type": "object", + "properties": { + "hostname": { + "type": "string", + "description": "Target hostname", + "nullable": true + }, + "requestedTypes": { + "type": "array", + "description": "Requested DNS record types", + "nullable": true, + "items": { + "type": "string", + "description": "Requested DNS record types", + "nullable": true + } + }, + "usedResolver": { + "type": "string", + "description": "Resolver used for lookup", + "nullable": true + }, + "records": { + "type": "object", + "additionalProperties": { + "type": "array", + "description": "Resolved DNS records keyed by record type", + "nullable": true, + "items": { + "type": "object", + "additionalProperties": { + "type": "object", + "description": "Resolved DNS records keyed by record type", + "nullable": true + }, + "description": "Resolved DNS records keyed by record type", + "nullable": true + } + }, + "description": "Resolved DNS records keyed by record type", + "nullable": true + }, + "attempts": { + "type": "array", + "description": "DNS resolution attempts", + "nullable": true, + "items": { + "type": "object", + "additionalProperties": { + "type": "object", + "description": "DNS resolution attempts", + "nullable": true + }, + "description": "DNS resolution attempts", + "nullable": true + } + }, + "failureKind": { + "type": "string", + "description": "Kind of DNS failure, if any", + "nullable": true + } + } + } + ] + }, "DnsExpectedCnameAssertion": { "required": [ "value" @@ -9618,7 +23896,10 @@ }, "EntitlementDto": { "required": [ - "key" + "key", + "value", + "defaultValue", + "overridden" ], "type": "object", "properties": { @@ -9650,7 +23931,10 @@ "name", "slug", "updatedAt", - "variables" + "variables", + "orgId", + "monitorCount", + "isDefault" ], "type": "object", "properties": { @@ -9704,6 +23988,39 @@ }, "description": "Environment with variable substitutions for monitor configs" }, + "ErrorResponse": { + "required": [ + "message", + "status", + "timestamp" + ], + "type": "object", + "properties": { + "status": { + "type": "integer", + "description": "HTTP status code (mirrors the response status line)", + "format": "int32", + "example": 404 + }, + "message": { + "type": "string", + "description": "Human-readable error message; safe to surface to end users", + "example": "Monitor not found" + }, + "timestamp": { + "type": "integer", + "description": "Server time when the error was produced (epoch milliseconds)", + "format": "int64", + "example": 1737302400000 + } + }, + "description": "Uniform error envelope returned for every non-2xx response", + "example": { + "status": 404, + "message": "Monitor not found", + "timestamp": 1737302400000 + } + }, "EscalationChain": { "required": [ "steps" @@ -9733,7 +24050,8 @@ }, "EscalationStep": { "required": [ - "channelIds" + "channelIds", + "delayMinutes" ], "type": "object", "properties": { @@ -9789,7 +24107,15 @@ }, "GlobalStatusSummaryDto": { "required": [ - "servicesWithIssues" + "servicesWithIssues", + "totalServices", + "operationalCount", + "degradedCount", + "partialOutageCount", + "majorOutageCount", + "maintenanceCount", + "unknownCount", + "activeIncidentCount" ], "type": "object", "properties": { @@ -10057,6 +24383,20 @@ } ] }, + "HeartbeatPingResponse": { + "required": [ + "ok" + ], + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "description": "Always true on a 2xx response", + "example": true + } + }, + "description": "Acknowledgement that a heartbeat ping was accepted" + }, "HeartbeatReceivedAssertion": { "type": "object", "allOf": [ @@ -10065,6 +24405,35 @@ } ] }, + "Http": { + "type": "object", + "description": "HTTP check-type-specific details", + "allOf": [ + { + "$ref": "#/components/schemas/CheckTypeDetailsDto" + }, + { + "type": "object", + "properties": { + "timing": { + "type": "object", + "additionalProperties": { + "type": "object", + "description": "Request phase timing breakdown", + "nullable": true + }, + "description": "Request phase timing breakdown", + "nullable": true + }, + "bodyTruncated": { + "type": "boolean", + "description": "Whether the response body was truncated before storage", + "nullable": true + } + } + } + ] + }, "HttpMonitorConfig": { "required": [ "method", @@ -10128,6 +24497,74 @@ } ] }, + "Icmp": { + "required": [ + "host" + ], + "type": "object", + "description": "ICMP (ping) check-type-specific details", + "allOf": [ + { + "$ref": "#/components/schemas/CheckTypeDetailsDto" + }, + { + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "Target host", + "example": "1.1.1.1" + }, + "packetsSent": { + "type": "integer", + "description": "Number of ICMP packets sent", + "format": "int32", + "nullable": true + }, + "packetsReceived": { + "type": "integer", + "description": "Number of ICMP packets received", + "format": "int32", + "nullable": true + }, + "packetLoss": { + "type": "number", + "description": "Packet loss percentage", + "format": "double", + "nullable": true, + "example": 0 + }, + "avgRttMs": { + "type": "number", + "description": "Average round-trip time in ms", + "format": "double", + "nullable": true + }, + "minRttMs": { + "type": "number", + "description": "Minimum round-trip time in ms", + "format": "double", + "nullable": true + }, + "maxRttMs": { + "type": "number", + "description": "Maximum round-trip time in ms", + "format": "double", + "nullable": true + }, + "jitterMs": { + "type": "number", + "description": "Jitter in ms", + "format": "double", + "nullable": true + } + }, + "required": [ + "host" + ] + } + ] + }, "IcmpMonitorConfig": { "required": [ "host" @@ -10274,7 +24711,10 @@ "severity", "source", "status", - "updatedAt" + "updatedAt", + "organizationId", + "reopenCount", + "statusPageVisible" ], "type": "object", "properties": { @@ -10434,22 +24874,22 @@ }, "monitorName": { "type": "string", - "description": "Name of the associated monitor; populated on list responses", + "description": "Name of the associated monitor; populated on list responses. Omitted from JSON (undefined to SDKs) on detail responses, treat missing as null.", "nullable": true }, "serviceName": { "type": "string", - "description": "Name of the associated service; populated on list responses", + "description": "Name of the associated service; populated on list responses. Omitted from JSON (undefined to SDKs) on detail responses, treat missing as null.", "nullable": true }, "serviceSlug": { "type": "string", - "description": "Slug of the associated service; populated on list responses", + "description": "Slug of the associated service; populated on list responses. Omitted from JSON (undefined to SDKs) on detail responses, treat missing as null.", "nullable": true }, "monitorType": { "type": "string", - "description": "Type of the associated monitor; populated on list responses", + "description": "Type of the associated monitor; populated on list responses. Omitted from JSON (undefined to SDKs) on detail responses, treat missing as null.", "nullable": true }, "resourceGroupId": { @@ -10460,7 +24900,7 @@ }, "resourceGroupName": { "type": "string", - "description": "Name of the resource group; populated on list responses", + "description": "Name of the resource group; populated on list responses. Omitted from JSON (undefined to SDKs) on detail responses, treat missing as null.", "nullable": true } }, @@ -10471,6 +24911,7 @@ "properties": { "status": { "type": "string", + "description": "Filter by incident lifecycle status; null returns every status", "nullable": true, "enum": [ "WATCHING", @@ -10481,6 +24922,7 @@ }, "severity": { "type": "string", + "description": "Filter by severity; null returns every severity", "nullable": true, "enum": [ "DOWN", @@ -10490,6 +24932,7 @@ }, "source": { "type": "string", + "description": "Filter by where the incident originated (auto, manual, third-party)", "nullable": true, "enum": [ "AUTOMATIC", @@ -10501,49 +24944,60 @@ }, "monitorId": { "type": "string", + "description": "Only return incidents tied to this monitor ID", "format": "uuid", "nullable": true }, "serviceId": { "type": "string", + "description": "Only return incidents tied to this service ID (third-party services)", "format": "uuid", "nullable": true }, "resourceGroupId": { "type": "string", + "description": "Only return incidents whose monitor belongs to this resource group", "format": "uuid", "nullable": true }, "tagId": { "type": "string", + "description": "Only return incidents whose monitor carries this tag", "format": "uuid", "nullable": true }, "environmentId": { "type": "string", + "description": "Only return incidents whose monitor lives in this environment", "format": "uuid", "nullable": true }, "startedFrom": { "type": "string", + "description": "Earliest startedAt to include (inclusive, ISO 8601)", "format": "date-time", "nullable": true }, "startedTo": { "type": "string", + "description": "Latest startedAt to include (inclusive, ISO 8601)", "format": "date-time", "nullable": true }, "page": { "minimum": 0, "type": "integer", - "format": "int32" + "description": "Zero-based page index (default: 0)", + "format": "int32", + "example": 0 }, "size": { "maximum": 200, "minimum": 1, "type": "integer", - "format": "int32" + "description": "Number of incidents per page (1–200, default: 10)", + "format": "int32", + "example": 10 } }, "required": [ @@ -10662,7 +25116,8 @@ "required": [ "createdAt", "id", - "incidentId" + "incidentId", + "notifySubscribers" ], "type": "object", "properties": { @@ -10837,7 +25292,8 @@ "required": [ "email", "expiresAt", - "roleOffered" + "roleOffered", + "inviteId" ], "type": "object", "properties": { @@ -10927,7 +25383,8 @@ "KeyInfo": { "required": [ "createdAt", - "name" + "name", + "id" ], "type": "object", "properties": { @@ -10968,7 +25425,8 @@ "statusPageId", "statusPageName", "statusPageSlug", - "title" + "title", + "scheduled" ], "type": "object", "properties": { @@ -11076,7 +25534,9 @@ "createdAt", "endsAt", "id", - "startsAt" + "startsAt", + "organizationId", + "suppressAlerts" ], "type": "object", "properties": { @@ -11293,6 +25753,58 @@ } ] }, + "McpServer": { + "type": "object", + "description": "MCP server check-type-specific details", + "allOf": [ + { + "$ref": "#/components/schemas/CheckTypeDetailsDto" + }, + { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "MCP server URL", + "nullable": true + }, + "protocolVersion": { + "type": "string", + "description": "MCP protocol version", + "nullable": true + }, + "serverInfo": { + "type": "object", + "additionalProperties": { + "type": "object", + "description": "MCP server info (name, version, etc.)", + "nullable": true + }, + "description": "MCP server info (name, version, etc.)", + "nullable": true + }, + "toolCount": { + "type": "integer", + "description": "Number of tools exposed", + "format": "int32", + "nullable": true + }, + "resourceCount": { + "type": "integer", + "description": "Number of resources exposed", + "format": "int32", + "nullable": true + }, + "promptCount": { + "type": "integer", + "description": "Number of prompts exposed", + "format": "int32", + "nullable": true + } + } + } + ] + }, "McpServerMonitorConfig": { "required": [ "command" @@ -11387,7 +25899,8 @@ "createdAt", "email", "orgRole", - "status" + "status", + "userId" ], "type": "object", "properties": { @@ -11705,7 +26218,27 @@ }, "MonitorConfig": { "type": "object", - "description": "Protocol-specific monitor configuration" + "description": "Protocol-specific monitor configuration; concrete type is deduced from the JSON shape", + "oneOf": [ + { + "$ref": "#/components/schemas/HttpMonitorConfig" + }, + { + "$ref": "#/components/schemas/DnsMonitorConfig" + }, + { + "$ref": "#/components/schemas/McpServerMonitorConfig" + }, + { + "$ref": "#/components/schemas/TcpMonitorConfig" + }, + { + "$ref": "#/components/schemas/IcmpMonitorConfig" + }, + { + "$ref": "#/components/schemas/HeartbeatMonitorConfig" + } + ] }, "MonitorDto": { "required": [ @@ -11716,7 +26249,10 @@ "name", "regions", "type", - "updatedAt" + "updatedAt", + "organizationId", + "frequencySeconds", + "enabled" ], "type": "object", "properties": { @@ -11747,26 +26283,7 @@ ] }, "config": { - "oneOf": [ - { - "$ref": "#/components/schemas/DnsMonitorConfig" - }, - { - "$ref": "#/components/schemas/HeartbeatMonitorConfig" - }, - { - "$ref": "#/components/schemas/HttpMonitorConfig" - }, - { - "$ref": "#/components/schemas/IcmpMonitorConfig" - }, - { - "$ref": "#/components/schemas/McpServerMonitorConfig" - }, - { - "$ref": "#/components/schemas/TcpMonitorConfig" - } - ] + "$ref": "#/components/schemas/MonitorConfig" }, "frequencySeconds": { "type": "integer", @@ -11951,26 +26468,7 @@ ] }, "config": { - "oneOf": [ - { - "$ref": "#/components/schemas/DnsMonitorConfig" - }, - { - "$ref": "#/components/schemas/HeartbeatMonitorConfig" - }, - { - "$ref": "#/components/schemas/HttpMonitorConfig" - }, - { - "$ref": "#/components/schemas/IcmpMonitorConfig" - }, - { - "$ref": "#/components/schemas/McpServerMonitorConfig" - }, - { - "$ref": "#/components/schemas/TcpMonitorConfig" - } - ] + "$ref": "#/components/schemas/MonitorConfig" }, "assertions": { "type": "array", @@ -11984,7 +26482,8 @@ }, "MonitorTestResultDto": { "required": [ - "assertionResults" + "assertionResults", + "passed" ], "type": "object", "properties": { @@ -12056,7 +26555,8 @@ "createdAt", "id", "monitorId", - "snapshot" + "snapshot", + "version" ], "type": "object", "properties": { @@ -12136,7 +26636,8 @@ "incidentId", "policyId", "status", - "updatedAt" + "updatedAt", + "currentStep" ], "type": "object", "properties": { @@ -12235,7 +26736,9 @@ "required": [ "createdAt", "title", - "type" + "type", + "id", + "read" ], "type": "object", "properties": { @@ -12286,7 +26789,10 @@ "id", "matchRules", "name", - "updatedAt" + "updatedAt", + "organizationId", + "enabled", + "priority" ], "type": "object", "properties": { @@ -12369,7 +26875,8 @@ "OrganizationDto": { "required": [ "email", - "name" + "name", + "id" ], "type": "object", "properties": { @@ -12407,7 +26914,8 @@ }, "OrgInfo": { "required": [ - "name" + "name", + "id" ], "type": "object", "properties": { @@ -12508,7 +27016,8 @@ "required": [ "entitlements", "tier", - "usage" + "usage", + "trialActive" ], "type": "object", "properties": { @@ -12560,7 +27069,8 @@ }, "PollChartBucketDto": { "required": [ - "bucket" + "bucket", + "totalPolls" ], "type": "object", "properties": { @@ -12782,7 +27292,8 @@ "RegionStatusDto": { "required": [ "region", - "timestamp" + "timestamp", + "passed" ], "type": "object", "properties": { @@ -12878,14 +27389,12 @@ "description": "Reorder page-level layout: groups and ungrouped components share one ordering" }, "ResolveIncidentRequest": { - "required": [ - "body" - ], "type": "object", "properties": { "body": { "type": "string", - "description": "Optional resolution message or post-mortem notes" + "description": "Optional resolution message or post-mortem notes", + "nullable": true } } }, @@ -12896,7 +27405,9 @@ "id", "name", "slug", - "updatedAt" + "updatedAt", + "organizationId", + "suppressMemberAlerts" ], "type": "object", "properties": { @@ -13026,7 +27537,10 @@ }, "ResourceGroupHealthDto": { "required": [ - "status" + "status", + "totalMembers", + "operationalCount", + "activeIncidents" ], "type": "object", "properties": { @@ -13307,7 +27821,9 @@ }, "RetryStrategy": { "required": [ - "type" + "type", + "maxRetries", + "interval" ], "type": "object", "properties": { @@ -13413,7 +27929,8 @@ "id", "key", "updatedAt", - "valueHash" + "valueHash", + "dekVersion" ], "type": "object", "properties": { @@ -13485,7 +28002,12 @@ "id", "name", "slug", - "updatedAt" + "updatedAt", + "pollingIntervalSeconds", + "enabled", + "published", + "componentCount", + "activeIncidentCount" ], "type": "object", "properties": { @@ -13569,7 +28091,12 @@ "lastSeenAt", "lifecycleStatus", "name", - "status" + "status", + "showcase", + "onlyShowIfDegraded", + "hasUptime", + "displayAggregatedUptime", + "isGroup" ], "type": "object", "properties": { @@ -13679,7 +28206,10 @@ "required": [ "components", "date", - "incidents" + "incidents", + "totalPartialOutageSeconds", + "totalMajorOutageSeconds", + "totalDegradedSeconds" ], "type": "object", "properties": { @@ -13704,6 +28234,11 @@ "description": "Sum of major outage seconds across all leaf components", "format": "int64" }, + "totalDegradedSeconds": { + "type": "integer", + "description": "Sum of degraded performance seconds across all leaf components", + "format": "int64" + }, "components": { "type": "array", "description": "Per-component impact rows for the day (only components with uptime data)", @@ -13732,7 +28267,9 @@ "name", "recentIncidents", "slug", - "updatedAt" + "updatedAt", + "pollingIntervalSeconds", + "enabled" ], "type": "object", "properties": { @@ -13983,7 +28520,8 @@ }, "ServiceLiveStatusDto": { "required": [ - "componentStatuses" + "componentStatuses", + "activeIncidentCount" ], "type": "object", "properties": { @@ -14014,7 +28552,10 @@ "ServicePollResultDto": { "required": [ "serviceId", - "timestamp" + "timestamp", + "passed", + "componentCount", + "degradedCount" ], "type": "object", "properties": { @@ -14076,7 +28617,9 @@ "ServicePollSummaryDto": { "required": [ "chartData", - "window" + "window", + "totalPolls", + "passedPolls" ], "type": "object", "properties": { @@ -14169,7 +28712,9 @@ "serviceId", "slug", "subscribedAt", - "subscriptionId" + "subscriptionId", + "pollingIntervalSeconds", + "enabled" ], "type": "object", "properties": { @@ -14390,6 +28935,17 @@ } } }, + "SingleValueResponseBatchComponentUptimeDto": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/BatchComponentUptimeDto" + } + } + }, "SingleValueResponseBulkMonitorActionResult": { "required": [ "data" @@ -14527,23 +29083,6 @@ } } }, - "SingleValueResponseMapStringListComponentUptimeDayDto": { - "required": [ - "data" - ], - "type": "object", - "properties": { - "data": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ComponentUptimeDayDto" - } - } - } - } - }, "SingleValueResponseMonitorAssertionDto": { "required": [ "data" @@ -15128,7 +29667,11 @@ "name", "statusPageId", "type", - "updatedAt" + "updatedAt", + "showUptime", + "displayOrder", + "pageOrder", + "excludeFromOverall" ], "type": "object", "properties": { @@ -15216,7 +29759,10 @@ "id", "name", "statusPageId", - "updatedAt" + "updatedAt", + "displayOrder", + "pageOrder", + "collapsed" ], "type": "object", "properties": { @@ -15263,6 +29809,46 @@ } } }, + "StatusPageComponentUptimeDayDto": { + "required": [ + "date", + "partialOutageSeconds", + "majorOutageSeconds", + "uptimePercentage" + ], + "type": "object", + "properties": { + "date": { + "type": "string", + "description": "Start-of-day timestamp for this bucket (UTC midnight)", + "format": "date-time" + }, + "partialOutageSeconds": { + "type": "integer", + "description": "Seconds of partial outage on this day", + "format": "int32" + }, + "majorOutageSeconds": { + "type": "integer", + "description": "Seconds of major outage on this day", + "format": "int32" + }, + "uptimePercentage": { + "type": "number", + "description": "Computed uptime percentage using weighted formula", + "format": "double" + }, + "incidents": { + "type": "array", + "description": "Incidents that overlapped this day", + "nullable": true, + "items": { + "$ref": "#/components/schemas/IncidentRef" + } + } + }, + "description": "Daily uptime data for a status page component" + }, "StatusPageCustomDomainDto": { "required": [ "createdAt", @@ -15272,7 +29858,8 @@ "updatedAt", "verificationCnameTarget", "verificationMethod", - "verificationToken" + "verificationToken", + "primary" ], "type": "object", "properties": { @@ -15339,7 +29926,10 @@ "name", "slug", "updatedAt", - "visibility" + "visibility", + "organizationId", + "workspaceId", + "enabled" ], "type": "object", "properties": { @@ -15456,7 +30046,9 @@ "status", "statusPageId", "title", - "updatedAt" + "updatedAt", + "scheduled", + "autoResolve" ], "type": "object", "properties": { @@ -15567,7 +30159,8 @@ "body", "createdAt", "id", - "status" + "status", + "notifySubscribers" ], "type": "object", "properties": { @@ -15613,7 +30206,8 @@ "required": [ "createdAt", "email", - "id" + "id", + "confirmed" ], "type": "object", "properties": { @@ -15658,7 +30252,9 @@ }, "TableValueResultAlertChannelDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -15688,7 +30284,9 @@ }, "TableValueResultAlertDeliveryDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -15718,7 +30316,9 @@ }, "TableValueResultApiKeyDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -15748,7 +30348,9 @@ }, "TableValueResultAuditEventDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -15778,7 +30380,9 @@ }, "TableValueResultCategoryDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -15808,7 +30412,9 @@ }, "TableValueResultComponentUptimeDayDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -15838,7 +30444,9 @@ }, "TableValueResultDeliveryAttemptDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -15868,7 +30476,9 @@ }, "TableValueResultEnvironmentDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -15898,7 +30508,9 @@ }, "TableValueResultIncidentDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -15928,7 +30540,9 @@ }, "TableValueResultIntegrationDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -15958,7 +30572,9 @@ }, "TableValueResultInviteDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -15988,7 +30604,9 @@ }, "TableValueResultMaintenanceWindowDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16018,7 +30636,9 @@ }, "TableValueResultMemberDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16048,7 +30668,9 @@ }, "TableValueResultMonitorDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16078,7 +30700,9 @@ }, "TableValueResultMonitorVersionDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16108,7 +30732,9 @@ }, "TableValueResultNotificationDispatchDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16138,7 +30764,9 @@ }, "TableValueResultNotificationDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16168,7 +30796,9 @@ }, "TableValueResultNotificationPolicyDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16198,7 +30828,9 @@ }, "TableValueResultResourceGroupDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16228,7 +30860,9 @@ }, "TableValueResultScheduledMaintenanceDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16258,7 +30892,9 @@ }, "TableValueResultSecretDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16288,7 +30924,9 @@ }, "TableValueResultServiceComponentDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16318,7 +30956,9 @@ }, "TableValueResultServiceIncidentDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16348,7 +30988,9 @@ }, "TableValueResultServiceSubscriptionDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16378,7 +31020,9 @@ }, "TableValueResultStatusPageComponentDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16408,7 +31052,9 @@ }, "TableValueResultStatusPageComponentGroupDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16436,9 +31082,43 @@ } } }, + "TableValueResultStatusPageComponentUptimeDayDto": { + "required": [ + "data", + "hasNext", + "hasPrev" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StatusPageComponentUptimeDayDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + }, + "totalElements": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "totalPages": { + "type": "integer", + "format": "int32", + "nullable": true + } + } + }, "TableValueResultStatusPageCustomDomainDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16468,7 +31148,9 @@ }, "TableValueResultStatusPageDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16498,7 +31180,9 @@ }, "TableValueResultStatusPageIncidentDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16528,7 +31212,9 @@ }, "TableValueResultStatusPageSubscriberDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16558,7 +31244,9 @@ }, "TableValueResultTagDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16588,7 +31276,9 @@ }, "TableValueResultWebhookDeliveryDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16618,7 +31308,9 @@ }, "TableValueResultWebhookEndpointDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16648,7 +31340,9 @@ }, "TableValueResultWorkspaceDto": { "required": [ - "data" + "data", + "hasNext", + "hasPrev" ], "type": "object", "properties": { @@ -16682,7 +31376,8 @@ "createdAt", "id", "name", - "updatedAt" + "updatedAt", + "organizationId" ], "type": "object", "properties": { @@ -16719,6 +31414,43 @@ }, "description": "Tag for organizing and filtering monitors" }, + "Tcp": { + "required": [ + "host" + ], + "type": "object", + "description": "TCP check-type-specific details", + "allOf": [ + { + "$ref": "#/components/schemas/CheckTypeDetailsDto" + }, + { + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "Target host", + "example": "db.example.com" + }, + "port": { + "type": "integer", + "description": "Target port", + "format": "int32", + "example": 5432 + }, + "connected": { + "type": "boolean", + "description": "Whether a TCP connection was established" + } + }, + "required": [ + "host", + "port", + "connected" + ] + } + ] + }, "TcpConnectsAssertion": { "type": "object", "allOf": [ @@ -16867,7 +31599,8 @@ }, "TestChannelResult": { "required": [ - "message" + "message", + "success" ], "type": "object", "properties": { @@ -16882,7 +31615,8 @@ "TestMatchResult": { "required": [ "matchedRules", - "unmatchedRules" + "unmatchedRules", + "matched" ], "type": "object", "properties": { @@ -18009,7 +32743,8 @@ }, "UptimeBucketDto": { "required": [ - "timestamp" + "timestamp", + "totalPolls" ], "type": "object", "properties": { @@ -18124,7 +32859,9 @@ "eventId", "eventType", "id", - "status" + "status", + "attemptCount", + "maxAttempts" ], "type": "object", "properties": { @@ -18194,7 +32931,9 @@ "id", "subscribedEvents", "updatedAt", - "url" + "url", + "enabled", + "consecutiveFailures" ], "type": "object", "properties": { @@ -18308,7 +33047,8 @@ }, "WebhookTestResult": { "required": [ - "message" + "message", + "success" ], "type": "object", "properties": { @@ -18334,7 +33074,9 @@ "required": [ "createdAt", "name", - "updatedAt" + "updatedAt", + "id", + "orgId" ], "type": "object", "properties": { diff --git a/src/devhelm/_generated.py b/src/devhelm/_generated.py index 26a6c34..287fd02 100644 --- a/src/devhelm/_generated.py +++ b/src/devhelm/_generated.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: .openapi-preprocessed.json -# timestamp: 2026-04-19T11:34:57+00:00 +# timestamp: 2026-04-20T10:53:24+00:00 from __future__ import annotations from typing import Annotated, Any @@ -645,6 +645,50 @@ class ComponentStatusDto(BaseModel): ] +class ComponentUptimeDayDto(BaseModel): + date: Annotated[ + AwareDatetime, Field(description="Date of the daily bucket (ISO 8601)") + ] + partial_outage_seconds: Annotated[ + int, + Field( + alias="partialOutageSeconds", + description="Seconds of partial outage observed on this day", + ), + ] + major_outage_seconds: Annotated[ + int, + Field( + alias="majorOutageSeconds", + description="Seconds of major outage observed on this day", + ), + ] + degraded_seconds: Annotated[ + int, + Field( + alias="degradedSeconds", + description="Seconds the component spent in degraded performance on this day", + ), + ] + uptime_percentage: Annotated[ + float, + Field( + alias="uptimePercentage", + description="Computed uptime percentage for the day", + ), + ] + events_json: Annotated[ + str | None, + Field( + alias="eventsJson", + description="Incident event references for this day as raw JSON", + ), + ] = None + source: Annotated[ + str, Field(description="Data source: vendor_reported or incident_derived") + ] + + class ComponentUptimeSummaryDto(BaseModel): day: Annotated[ float | None, @@ -1100,23 +1144,6 @@ class CreateWorkspaceRequest(BaseModel): name: Annotated[str, Field(description="Workspace name", min_length=1)] -class CursorPage(BaseModel): - data: Annotated[list[dict[str, Any]], Field(description="Items on this page")] - next_cursor: Annotated[ - str | None, - Field( - alias="nextCursor", - description="Opaque cursor for the next page; null when there are no more results", - ), - ] = None - has_more: Annotated[ - bool, - Field( - alias="hasMore", description="Whether more results exist beyond this page" - ), - ] - - class DayIncident(BaseModel): id: Annotated[UUID, Field(description="Status page incident UUID")] title: Annotated[str, Field(description="Incident title")] @@ -1293,6 +1320,29 @@ class DiscordChannelConfig(ChannelConfig): ] = None +class Dns(CheckTypeDetailsDto): + hostname: Annotated[str | None, Field(description="Target hostname")] = None + requested_types: Annotated[ + list[str] | None, + Field(alias="requestedTypes", description="Requested DNS record types"), + ] = None + used_resolver: Annotated[ + str | None, Field(alias="usedResolver", description="Resolver used for lookup") + ] = None + records: Annotated[ + dict[str, list[dict[str, dict[str, Any]]]] | None, + Field(description="Resolved DNS records keyed by record type"), + ] = None + attempts: Annotated[ + list[dict[str, dict[str, Any]]] | None, + Field(description="DNS resolution attempts"), + ] = None + failure_kind: Annotated[ + str | None, + Field(alias="failureKind", description="Kind of DNS failure, if any"), + ] = None + + class DnsExpectedCnameAssertion(AssertionConfig): value: Annotated[ str, @@ -1508,6 +1558,30 @@ class EnvironmentDto(BaseModel): ] +class ErrorResponse(BaseModel): + status: Annotated[ + int, + Field( + description="HTTP status code (mirrors the response status line)", + examples=[404], + ), + ] + message: Annotated[ + str, + Field( + description="Human-readable error message; safe to surface to end users", + examples=["Monitor not found"], + ), + ] + timestamp: Annotated[ + int, + Field( + description="Server time when the error was produced (epoch milliseconds)", + examples=[1737302400000], + ), + ] + + class EscalationStep(BaseModel): delay_minutes: Annotated[ int, @@ -1627,10 +1701,30 @@ class HeartbeatPayloadContainsAssertion(AssertionConfig): ] +class HeartbeatPingResponse(BaseModel): + ok: Annotated[ + bool, Field(description="Always true on a 2xx response", examples=[True]) + ] + + class HeartbeatReceivedAssertion(AssertionConfig): pass +class Http(CheckTypeDetailsDto): + timing: Annotated[ + dict[str, dict[str, Any]] | None, + Field(description="Request phase timing breakdown"), + ] = None + body_truncated: Annotated[ + bool | None, + Field( + alias="bodyTruncated", + description="Whether the response body was truncated before storage", + ), + ] = None + + class Method(StrEnum): get = "GET" post = "POST" @@ -1640,6 +1734,37 @@ class Method(StrEnum): head = "HEAD" +class Icmp(CheckTypeDetailsDto): + host: Annotated[str, Field(description="Target host", examples=["1.1.1.1"])] + packets_sent: Annotated[ + int | None, + Field(alias="packetsSent", description="Number of ICMP packets sent"), + ] = None + packets_received: Annotated[ + int | None, + Field(alias="packetsReceived", description="Number of ICMP packets received"), + ] = None + packet_loss: Annotated[ + float | None, + Field(alias="packetLoss", description="Packet loss percentage", examples=[0]), + ] = None + avg_rtt_ms: Annotated[ + float | None, + Field(alias="avgRttMs", description="Average round-trip time in ms"), + ] = None + min_rtt_ms: Annotated[ + float | None, + Field(alias="minRttMs", description="Minimum round-trip time in ms"), + ] = None + max_rtt_ms: Annotated[ + float | None, + Field(alias="maxRttMs", description="Maximum round-trip time in ms"), + ] = None + jitter_ms: Annotated[ + float | None, Field(alias="jitterMs", description="Jitter in ms") + ] = None + + class IcmpPacketLossAssertion(AssertionConfig): max_percent: Annotated[ float, @@ -1845,28 +1970,28 @@ class IncidentDto(BaseModel): str | None, Field( alias="monitorName", - description="Name of the associated monitor; populated on list responses", + description="Name of the associated monitor; populated on list responses. Omitted from JSON (undefined to SDKs) on detail responses, treat missing as null.", ), ] = None service_name: Annotated[ str | None, Field( alias="serviceName", - description="Name of the associated service; populated on list responses", + description="Name of the associated service; populated on list responses. Omitted from JSON (undefined to SDKs) on detail responses, treat missing as null.", ), ] = None service_slug: Annotated[ str | None, Field( alias="serviceSlug", - description="Slug of the associated service; populated on list responses", + description="Slug of the associated service; populated on list responses. Omitted from JSON (undefined to SDKs) on detail responses, treat missing as null.", ), ] = None monitor_type: Annotated[ str | None, Field( alias="monitorType", - description="Type of the associated monitor; populated on list responses", + description="Type of the associated monitor; populated on list responses. Omitted from JSON (undefined to SDKs) on detail responses, treat missing as null.", ), ] = None resource_group_id: Annotated[ @@ -1880,24 +2005,89 @@ class IncidentDto(BaseModel): str | None, Field( alias="resourceGroupName", - description="Name of the resource group; populated on list responses", + description="Name of the resource group; populated on list responses. Omitted from JSON (undefined to SDKs) on detail responses, treat missing as null.", ), ] = None class IncidentFilterParams(BaseModel): - status: Status6 | None = None - severity: Severity3 | None = None - source: Source | None = None - monitor_id: Annotated[UUID | None, Field(alias="monitorId")] = None - service_id: Annotated[UUID | None, Field(alias="serviceId")] = None - resource_group_id: Annotated[UUID | None, Field(alias="resourceGroupId")] = None - tag_id: Annotated[UUID | None, Field(alias="tagId")] = None - environment_id: Annotated[UUID | None, Field(alias="environmentId")] = None - started_from: Annotated[AwareDatetime | None, Field(alias="startedFrom")] = None - started_to: Annotated[AwareDatetime | None, Field(alias="startedTo")] = None - page: Annotated[int, Field(ge=0)] - size: Annotated[int, Field(ge=1, le=200)] + status: Annotated[ + Status6 | None, + Field( + description="Filter by incident lifecycle status; null returns every status" + ), + ] = None + severity: Annotated[ + Severity3 | None, + Field(description="Filter by severity; null returns every severity"), + ] = None + source: Annotated[ + Source | None, + Field( + description="Filter by where the incident originated (auto, manual, third-party)" + ), + ] = None + monitor_id: Annotated[ + UUID | None, + Field( + alias="monitorId", + description="Only return incidents tied to this monitor ID", + ), + ] = None + service_id: Annotated[ + UUID | None, + Field( + alias="serviceId", + description="Only return incidents tied to this service ID (third-party services)", + ), + ] = None + resource_group_id: Annotated[ + UUID | None, + Field( + alias="resourceGroupId", + description="Only return incidents whose monitor belongs to this resource group", + ), + ] = None + tag_id: Annotated[ + UUID | None, + Field( + alias="tagId", + description="Only return incidents whose monitor carries this tag", + ), + ] = None + environment_id: Annotated[ + UUID | None, + Field( + alias="environmentId", + description="Only return incidents whose monitor lives in this environment", + ), + ] = None + started_from: Annotated[ + AwareDatetime | None, + Field( + alias="startedFrom", + description="Earliest startedAt to include (inclusive, ISO 8601)", + ), + ] = None + started_to: Annotated[ + AwareDatetime | None, + Field( + alias="startedTo", + description="Latest startedAt to include (inclusive, ISO 8601)", + ), + ] = None + page: Annotated[ + int, Field(description="Zero-based page index (default: 0)", examples=[0], ge=0) + ] + size: Annotated[ + int, + Field( + description="Number of incidents per page (1–200, default: 10)", + examples=[10], + ge=1, + le=200, + ), + ] class IncidentRef(BaseModel): @@ -2185,6 +2375,27 @@ class McpResponseTimeWarnAssertion(AssertionConfig): ] +class McpServer(CheckTypeDetailsDto): + url: Annotated[str | None, Field(description="MCP server URL")] = None + protocol_version: Annotated[ + str | None, Field(alias="protocolVersion", description="MCP protocol version") + ] = None + server_info: Annotated[ + dict[str, dict[str, Any]] | None, + Field(alias="serverInfo", description="MCP server info (name, version, etc.)"), + ] = None + tool_count: Annotated[ + int | None, Field(alias="toolCount", description="Number of tools exposed") + ] = None + resource_count: Annotated[ + int | None, + Field(alias="resourceCount", description="Number of resources exposed"), + ] = None + prompt_count: Annotated[ + int | None, Field(alias="promptCount", description="Number of prompts exposed") + ] = None + + class McpToolAvailableAssertion(AssertionConfig): tool_name: Annotated[ str, @@ -2789,8 +3000,9 @@ class ReorderPageLayoutRequest(BaseModel): class ResolveIncidentRequest(BaseModel): body: Annotated[ - str, Field(description="Optional resolution message or post-mortem notes") - ] + str | None, + Field(description="Optional resolution message or post-mortem notes"), + ] = None class Status12(StrEnum): @@ -3264,6 +3476,13 @@ class ServiceDayDetailDto(BaseModel): description="Sum of major outage seconds across all leaf components", ), ] + total_degraded_seconds: Annotated[ + int, + Field( + alias="totalDegradedSeconds", + description="Sum of degraded performance seconds across all leaf components", + ), + ] components: Annotated[ list[ComponentImpact], Field( @@ -3837,6 +4056,38 @@ class StatusPageComponentGroupDto(BaseModel): updated_at: Annotated[AwareDatetime, Field(alias="updatedAt")] +class StatusPageComponentUptimeDayDto(BaseModel): + date: Annotated[ + AwareDatetime, + Field(description="Start-of-day timestamp for this bucket (UTC midnight)"), + ] + partial_outage_seconds: Annotated[ + int, + Field( + alias="partialOutageSeconds", + description="Seconds of partial outage on this day", + ), + ] + major_outage_seconds: Annotated[ + int, + Field( + alias="majorOutageSeconds", + description="Seconds of major outage on this day", + ), + ] + uptime_percentage: Annotated[ + float, + Field( + alias="uptimePercentage", + description="Computed uptime percentage using weighted formula", + ), + ] + incidents: Annotated[ + list[IncidentRef] | None, + Field(description="Incidents that overlapped this day"), + ] = None + + class Status14(StrEnum): pending_verification = "PENDING_VERIFICATION" verification_failed = "VERIFICATION_FAILED" @@ -3983,6 +4234,14 @@ class TableValueResultCategoryDto(BaseModel): total_pages: Annotated[int | None, Field(alias="totalPages")] = None +class TableValueResultComponentUptimeDayDto(BaseModel): + data: list[ComponentUptimeDayDto] + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] + total_elements: Annotated[int | None, Field(alias="totalElements")] = None + total_pages: Annotated[int | None, Field(alias="totalPages")] = None + + class TableValueResultDeliveryAttemptDto(BaseModel): data: list[DeliveryAttemptDto] has_next: Annotated[bool, Field(alias="hasNext")] @@ -4103,6 +4362,14 @@ class TableValueResultStatusPageComponentGroupDto(BaseModel): total_pages: Annotated[int | None, Field(alias="totalPages")] = None +class TableValueResultStatusPageComponentUptimeDayDto(BaseModel): + data: list[StatusPageComponentUptimeDayDto] + has_next: Annotated[bool, Field(alias="hasNext")] + has_prev: Annotated[bool, Field(alias="hasPrev")] + total_elements: Annotated[int | None, Field(alias="totalElements")] = None + total_pages: Annotated[int | None, Field(alias="totalPages")] = None + + class TableValueResultStatusPageCustomDomainDto(BaseModel): data: list[StatusPageCustomDomainDto] has_next: Annotated[bool, Field(alias="hasNext")] @@ -4150,6 +4417,14 @@ class TagDto(BaseModel): ] +class Tcp(CheckTypeDetailsDto): + host: Annotated[str, Field(description="Target host", examples=["db.example.com"])] + port: Annotated[int, Field(description="Target port", examples=[5432])] + connected: Annotated[ + bool, Field(description="Whether a TCP connection was established") + ] + + class TcpConnectsAssertion(AssertionConfig): pass @@ -5178,6 +5453,15 @@ class BasicAuthConfig(MonitorAuthConfig): ] = None +class BatchComponentUptimeDto(BaseModel): + components: Annotated[ + dict[str, list[ComponentUptimeDayDto]], + Field( + description="Map of component ID → list of per-day uptime entries (oldest → newest)" + ), + ] + + class BearerAuthConfig(MonitorAuthConfig): vault_secret_id: Annotated[ UUID | None, @@ -5200,9 +5484,7 @@ class BulkMonitorActionRequest(BaseModel): ] action: Annotated[ Action, - Field( - description="Action to perform: PAUSE, RESUME, DELETE, ADD_TAG, REMOVE_TAG" - ), + Field(description="Action to apply to every monitor in the bulk request"), ] tag_ids: Annotated[ list[UUID] | None, @@ -5281,7 +5563,7 @@ class CheckResultDetailsDto(BaseModel): ), ] = None check_details: Annotated[ - CheckTypeDetailsDto | None, Field(alias="checkDetails") + Dns | Http | Icmp | McpServer | Tcp | None, Field(alias="checkDetails") ] = None @@ -5329,38 +5611,6 @@ class CheckResultDto(BaseModel): ] = None -class ComponentUptimeDayDto(BaseModel): - date: Annotated[ - AwareDatetime, - Field(description="Start-of-day timestamp for this bucket (UTC midnight)"), - ] - partial_outage_seconds: Annotated[ - int, - Field( - alias="partialOutageSeconds", - description="Seconds of partial outage on this day", - ), - ] - major_outage_seconds: Annotated[ - int, - Field( - alias="majorOutageSeconds", - description="Seconds of major outage on this day", - ), - ] - uptime_percentage: Annotated[ - float, - Field( - alias="uptimePercentage", - description="Computed uptime percentage using weighted formula", - ), - ] - incidents: Annotated[ - list[IncidentRef] | None, - Field(description="Incidents that overlapped this day"), - ] = None - - class CreateAlertChannelRequest(BaseModel): name: Annotated[ str, @@ -5427,11 +5677,11 @@ class CreateAssertionRequest(BaseModel): | TcpResponseTimeWarnAssertion ) severity: Annotated[ - Severity, + Severity | None, Field( - description="Outcome severity: FAIL (fails the check) or WARN (warns without failing)" + description="Outcome severity: FAIL (fails the check) or WARN (warns without failing, default: FAIL)" ), - ] + ] = None class CreateResourceGroupRequest(BaseModel): @@ -5981,14 +6231,7 @@ class MonitorDto(BaseModel): str, Field(description="Human-readable name for this monitor", min_length=1) ] type: Type3 - config: ( - DnsMonitorConfig - | HeartbeatMonitorConfig - | HttpMonitorConfig - | IcmpMonitorConfig - | McpServerMonitorConfig - | TcpMonitorConfig - ) + config: MonitorConfig frequency_seconds: Annotated[ int, Field( @@ -6048,14 +6291,7 @@ class MonitorDto(BaseModel): class MonitorTestRequest(BaseModel): type: Annotated[Type3, Field(description="Monitor protocol type to test")] - config: ( - DnsMonitorConfig - | HeartbeatMonitorConfig - | HttpMonitorConfig - | IcmpMonitorConfig - | McpServerMonitorConfig - | TcpMonitorConfig - ) + config: MonitorConfig assertions: Annotated[ list[CreateAssertionRequest] | None, Field(description="Optional assertions to evaluate against the test result"), @@ -6311,6 +6547,10 @@ class SingleValueResponseAuthMeResponse(BaseModel): data: AuthMeResponse +class SingleValueResponseBatchComponentUptimeDto(BaseModel): + data: BatchComponentUptimeDto + + class SingleValueResponseBulkMonitorActionResult(BaseModel): data: BulkMonitorActionResult @@ -6331,10 +6571,6 @@ class SingleValueResponseIncidentPolicyDto(BaseModel): data: IncidentPolicyDto -class SingleValueResponseMapStringListComponentUptimeDayDto(BaseModel): - data: dict[str, list[ComponentUptimeDayDto]] - - class SingleValueResponseMonitorAssertionDto(BaseModel): data: MonitorAssertionDto @@ -6450,14 +6686,6 @@ class StatusPageIncidentDto(BaseModel): updated_at: Annotated[AwareDatetime, Field(alias="updatedAt")] -class TableValueResultComponentUptimeDayDto(BaseModel): - data: list[ComponentUptimeDayDto] - has_next: Annotated[bool, Field(alias="hasNext")] - has_prev: Annotated[bool, Field(alias="hasPrev")] - total_elements: Annotated[int | None, Field(alias="totalElements")] = None - total_pages: Annotated[int | None, Field(alias="totalPages")] = None - - class TableValueResultIntegrationDto(BaseModel): data: list[IntegrationDto] has_next: Annotated[bool, Field(alias="hasNext")] @@ -6682,14 +6910,7 @@ class CreateMonitorRequest(BaseModel): ), ] type: Annotated[Type1, Field(description="Monitor protocol type")] - config: ( - DnsMonitorConfig - | HeartbeatMonitorConfig - | HttpMonitorConfig - | IcmpMonitorConfig - | McpServerMonitorConfig - | TcpMonitorConfig - ) + config: MonitorConfig frequency_seconds: Annotated[ int | None, Field( @@ -6747,22 +6968,22 @@ class CreateNotificationPolicyRequest(BaseModel): ), ] match_rules: Annotated[ - list[MatchRule], + list[MatchRule] | None, Field( alias="matchRules", description="Match rules to evaluate (all must pass; omit or empty for catch-all)", ), - ] + ] = None escalation: EscalationChain enabled: Annotated[ - bool, Field(description="Whether this policy is enabled (default true)") - ] + bool | None, Field(description="Whether this policy is enabled (default true)") + ] = True priority: Annotated[ - int, + int | None, Field( description="Evaluation priority; higher value = evaluated first (default 0)" ), - ] + ] = 0 class SingleValueResponseStatusPageIncidentDto(BaseModel): diff --git a/src/devhelm/_http.py b/src/devhelm/_http.py index e5593a2..12e2abe 100644 --- a/src/devhelm/_http.py +++ b/src/devhelm/_http.py @@ -103,9 +103,7 @@ def api_get( def api_post( - client: httpx.Client, - path: str, - body: BaseModel | dict[str, object] | None = None, + client: httpx.Client, path: str, body: BaseModel | dict[str, object] | None = None ) -> _JsonResponse: if body is None: return checked_fetch(client.post(path)) diff --git a/src/devhelm/_pagination.py b/src/devhelm/_pagination.py index 6396836..a5db2dc 100644 --- a/src/devhelm/_pagination.py +++ b/src/devhelm/_pagination.py @@ -66,8 +66,12 @@ def fetch_page( data=items, has_next=bool(resp.get("hasNext")) if isinstance(resp, dict) else False, has_prev=bool(resp.get("hasPrev")) if isinstance(resp, dict) else False, - total_elements=cast(int | None, resp.get("totalElements")) if isinstance(resp, dict) else None, - total_pages=cast(int | None, resp.get("totalPages")) if isinstance(resp, dict) else None, + total_elements=cast(int | None, resp.get("totalElements")) + if isinstance(resp, dict) + else None, + total_pages=cast(int | None, resp.get("totalPages")) + if isinstance(resp, dict) + else None, ) @@ -90,6 +94,8 @@ def fetch_cursor_page( items = parse_list(model_class, raw_items, f"GET {path}") return CursorPage( data=items, - next_cursor=cast(str | None, resp.get("nextCursor")) if isinstance(resp, dict) else None, + next_cursor=cast(str | None, resp.get("nextCursor")) + if isinstance(resp, dict) + else None, has_more=bool(resp.get("hasMore")) if isinstance(resp, dict) else False, ) diff --git a/tests/test_http.py b/tests/test_http.py index 742502c..0652d6a 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -9,7 +9,6 @@ from devhelm._http import DevhelmConfig, build_client, path_param from devhelm._validation import parse_list, parse_model, parse_single - # ---------- path_param ---------- diff --git a/tests/test_negative_validation.py b/tests/test_negative_validation.py index ce1e35c..5c4b71b 100644 --- a/tests/test_negative_validation.py +++ b/tests/test_negative_validation.py @@ -8,6 +8,7 @@ from __future__ import annotations +from typing import Any from uuid import uuid4 import pytest @@ -92,8 +93,8 @@ # --------------------------------------------------------------------------- -def _monitor(**kw: object) -> dict: - base: dict = { +def _monitor(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": UID, "organizationId": 1, "name": "M", @@ -110,8 +111,8 @@ def _monitor(**kw: object) -> dict: return base -def _incident(**kw: object) -> dict: - base: dict = { +def _incident(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": UID, "organizationId": 1, "source": "MANUAL", @@ -128,8 +129,8 @@ def _incident(**kw: object) -> dict: return base -def _alert_channel(**kw: object) -> dict: - base: dict = { +def _alert_channel(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": UID, "name": "ch", "channelType": "slack", @@ -140,8 +141,8 @@ def _alert_channel(**kw: object) -> dict: return base -def _api_key(**kw: object) -> dict: - base: dict = { +def _api_key(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": 1, "name": "k", "key": "dh_live_x", @@ -152,8 +153,8 @@ def _api_key(**kw: object) -> dict: return base -def _environment(**kw: object) -> dict: - base: dict = { +def _environment(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": UID, "orgId": 1, "name": "prod", @@ -168,8 +169,8 @@ def _environment(**kw: object) -> dict: return base -def _secret(**kw: object) -> dict: - base: dict = { +def _secret(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": UID, "key": "MY_KEY", "dekVersion": 1, @@ -181,8 +182,8 @@ def _secret(**kw: object) -> dict: return base -def _tag(**kw: object) -> dict: - base: dict = { +def _tag(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": UID, "organizationId": 1, "name": "prod", @@ -194,8 +195,8 @@ def _tag(**kw: object) -> dict: return base -def _webhook(**kw: object) -> dict: - base: dict = { +def _webhook(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": UID, "url": "https://hook.example.com", "subscribedEvents": ["monitor.created"], @@ -208,14 +209,19 @@ def _webhook(**kw: object) -> dict: return base -def _deploy_lock(**kw: object) -> dict: - base: dict = {"id": UID, "lockedBy": "ci-job-42", "lockedAt": NOW, "expiresAt": NOW} +def _deploy_lock(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { + "id": UID, + "lockedBy": "ci-job-42", + "lockedAt": NOW, + "expiresAt": NOW, + } base.update(kw) return base -def _resource_group(**kw: object) -> dict: - base: dict = { +def _resource_group(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": UID, "organizationId": 1, "name": "rg", @@ -234,8 +240,8 @@ def _resource_group(**kw: object) -> dict: return base -def _notification_policy(**kw: object) -> dict: - base: dict = { +def _notification_policy(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": UID, "organizationId": 1, "name": "np", @@ -250,8 +256,8 @@ def _notification_policy(**kw: object) -> dict: return base -def _status_page(**kw: object) -> dict: - base: dict = { +def _status_page(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": UID, "organizationId": 1, "workspaceId": 1, @@ -268,8 +274,8 @@ def _status_page(**kw: object) -> dict: return base -def _sp_component(**kw: object) -> dict: - base: dict = { +def _sp_component(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": UID, "statusPageId": UID, "name": "API", @@ -286,8 +292,8 @@ def _sp_component(**kw: object) -> dict: return base -def _sp_group(**kw: object) -> dict: - base: dict = { +def _sp_group(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": UID, "statusPageId": UID, "name": "Infra", @@ -301,8 +307,8 @@ def _sp_group(**kw: object) -> dict: return base -def _sp_incident(**kw: object) -> dict: - base: dict = { +def _sp_incident(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": UID, "statusPageId": UID, "title": "Down", @@ -318,8 +324,8 @@ def _sp_incident(**kw: object) -> dict: return base -def _sp_incident_update(**kw: object) -> dict: - base: dict = { +def _sp_incident_update(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": UID, "status": "INVESTIGATING", "body": "Looking into it", @@ -330,14 +336,19 @@ def _sp_incident_update(**kw: object) -> dict: return base -def _sp_subscriber(**kw: object) -> dict: - base: dict = {"id": UID, "email": "a@b.com", "confirmed": True, "createdAt": NOW} +def _sp_subscriber(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { + "id": UID, + "email": "a@b.com", + "confirmed": True, + "createdAt": NOW, + } base.update(kw) return base -def _sp_custom_domain(**kw: object) -> dict: - base: dict = { +def _sp_custom_domain(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": UID, "hostname": "status.example.com", "status": "ACTIVE", @@ -352,8 +363,8 @@ def _sp_custom_domain(**kw: object) -> dict: return base -def _sp_incident_component(**kw: object) -> dict: - base: dict = { +def _sp_incident_component(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "statusPageComponentId": UID, "componentStatus": "OPERATIONAL", "componentName": "API", @@ -362,14 +373,19 @@ def _sp_incident_component(**kw: object) -> dict: return base -def _check_result(**kw: object) -> dict: - base: dict = {"id": UID, "timestamp": NOW, "region": "us-east", "passed": True} +def _check_result(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { + "id": UID, + "timestamp": NOW, + "region": "us-east", + "passed": True, + } base.update(kw) return base -def _incident_policy(**kw: object) -> dict: - base: dict = { +def _incident_policy(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": UID, "monitorId": UID, "triggerRules": [ @@ -392,8 +408,8 @@ def _incident_policy(**kw: object) -> dict: return base -def _monitor_version(**kw: object) -> dict: - base: dict = { +def _monitor_version(**kw: object) -> dict[str, Any]: + base: dict[str, Any] = { "id": UID, "monitorId": UID, "version": 1, @@ -405,7 +421,7 @@ def _monitor_version(**kw: object) -> dict: return base -def _del(d: dict, key: str) -> dict: +def _del(d: dict[str, Any], key: str) -> dict[str, Any]: c = dict(d) del c[key] return c @@ -904,44 +920,24 @@ def test_missing_name(self) -> None: } ) - def test_missing_match_rules(self) -> None: - with pytest.raises(ValidationError, match="matchRules"): - CreateNotificationPolicyRequest.model_validate( - { - "name": "np", - "escalation": {"steps": [{"delayMinutes": 0, "channelIds": [UID]}]}, - "enabled": True, - "priority": 0, - } - ) - def test_missing_escalation(self) -> None: with pytest.raises(ValidationError, match="escalation"): CreateNotificationPolicyRequest.model_validate( {"name": "np", "matchRules": [], "enabled": True, "priority": 0} ) - def test_missing_enabled(self) -> None: - with pytest.raises(ValidationError, match="enabled"): - CreateNotificationPolicyRequest.model_validate( - { - "name": "np", - "matchRules": [], - "escalation": {"steps": [{"delayMinutes": 0, "channelIds": [UID]}]}, - "priority": 0, - } - ) - - def test_missing_priority(self) -> None: - with pytest.raises(ValidationError, match="priority"): - CreateNotificationPolicyRequest.model_validate( - { - "name": "np", - "matchRules": [], - "escalation": {"steps": [{"delayMinutes": 0, "channelIds": [UID]}]}, - "enabled": True, - } - ) + def test_missing_optional_fields_accepted(self) -> None: + """matchRules / enabled / priority are optional in the spec; only name + + escalation are required. The model must accept payloads without them.""" + model = CreateNotificationPolicyRequest.model_validate( + { + "name": "np", + "escalation": {"steps": [{"delayMinutes": 0, "channelIds": [UID]}]}, + } + ) + assert model.match_rules is None + assert model.enabled is None or model.enabled is True + assert model.priority is None or model.priority == 0 def test_null_name(self) -> None: with pytest.raises(ValidationError): @@ -2781,13 +2777,15 @@ def test_invalid_component_id(self) -> None: class TestResolveIncidentRequestNegative: - def test_missing_body(self) -> None: - with pytest.raises(ValidationError, match="body"): - ResolveIncidentRequest.model_validate({}) - - def test_null_body(self) -> None: - with pytest.raises(ValidationError): - ResolveIncidentRequest.model_validate({"body": None}) + def test_missing_body_accepted(self) -> None: + """body is optional (nullable) — empty payload should be accepted.""" + model = ResolveIncidentRequest.model_validate({}) + assert model.body is None + + def test_null_body_accepted(self) -> None: + """Explicit null body matches the spec's nullable=true and should be ok.""" + model = ResolveIncidentRequest.model_validate({"body": None}) + assert model.body is None def test_wrong_body_type(self) -> None: with pytest.raises(ValidationError): diff --git a/tests/test_schemas.py b/tests/test_schemas.py index bfad2c4..5888a8f 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -201,9 +201,9 @@ def test_valid(self) -> None: r = ResolveIncidentRequest.model_validate({"body": "All clear"}) assert r.body == "All clear" - def test_missing_body_raises(self) -> None: - with pytest.raises(ValidationError, match="body"): - ResolveIncidentRequest.model_validate({}) + def test_missing_body_accepted(self) -> None: + r = ResolveIncidentRequest.model_validate({}) + assert r.body is None class TestCreateManualIncidentRequest: diff --git a/tests/test_spec_parity.py b/tests/test_spec_parity.py index 8e9b67c..e3e289e 100644 --- a/tests/test_spec_parity.py +++ b/tests/test_spec_parity.py @@ -55,17 +55,20 @@ @pytest.fixture(scope="module") def spec() -> dict[str, Any]: """Load the vendored OpenAPI spec once per module.""" - return json.loads(SPEC_PATH.read_text()) + loaded: dict[str, Any] = json.loads(SPEC_PATH.read_text()) + return loaded @pytest.fixture(scope="module") def schemas(spec: dict[str, Any]) -> dict[str, Any]: - return spec["components"]["schemas"] + result: dict[str, Any] = spec["components"]["schemas"] + return result @pytest.fixture(scope="module") def paths(spec: dict[str, Any]) -> dict[str, Any]: - return spec["paths"] + result: dict[str, Any] = spec["paths"] + return result # ---------- 1. Schema parity: every public DTO must exist in the spec ---------- diff --git a/tests/test_typing.py b/tests/test_typing.py new file mode 100644 index 0000000..e8a91d3 --- /dev/null +++ b/tests/test_typing.py @@ -0,0 +1,66 @@ +"""Lock in strict typing posture for the SDK. + +These tests ensure that: + * mypy strict + ``disallow_any_explicit`` runs cleanly across the entire + package, including the previously-excluded ``_generated.py``. + * The single justified ``# type: ignore`` comment is the only one in + hand-written code (generated code may add ``[assignment]`` for the + ``HealthThresholdType.count`` collision documented in ``scripts/typegen.sh``). + +Skipped automatically if mypy is unavailable in the runtime environment (e.g. +when shipping the wheel without the ``dev`` group installed). +""" + +from __future__ import annotations + +import re +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parent.parent +SRC = ROOT / "src" / "devhelm" + + +def _have_mypy() -> bool: + return shutil.which("mypy") is not None + + +@pytest.mark.skipif(not _have_mypy(), reason="mypy not installed") +def test_mypy_strict_passes_including_generated() -> None: + result = subprocess.run( # noqa: S603 — fixed argv, no shell. + ["mypy", "src"], cwd=ROOT, check=False, capture_output=True, text=True + ) + assert result.returncode == 0, ( + "mypy strict (with disallow_any_explicit) must pass for the entire " + "package including the generated models.\n" + f"stdout:\n{result.stdout}\n\nstderr:\n{result.stderr}" + ) + + +def test_handwritten_modules_have_only_documented_type_ignores() -> None: + """Catch sneaky `# type: ignore` additions outside the generated file.""" + pattern = re.compile(r"#\s*type:\s*ignore") + offenders: list[str] = [] + for path in SRC.rglob("*.py"): + if path.name == "_generated.py": + continue + text = path.read_text() + for lineno, line in enumerate(text.splitlines(), start=1): + if pattern.search(line): + offenders.append(f"{path.relative_to(ROOT)}:{lineno}: {line.strip()}") + expected = { + "src/devhelm/_validation.py:79: adapter: TypeAdapter[list[M]] = " + "TypeAdapter(list[model_class]) # type: ignore[valid-type]", + "src/devhelm/_http.py:95: return response.json() # type: " + "ignore[no-any-return]", + } + actual = set(offenders) + extra = actual - expected + assert not extra, ( + "Unexpected `# type: ignore` comments outside the generated file. " + "Each suppression must be documented and added to the test allow-list:\n" + + "\n".join(sorted(extra)) + ) From 52416d24422459e74e842dafca5c3aedf66ad96c Mon Sep 17 00:00:00 2001 From: caballeto Date: Mon, 20 Apr 2026 14:59:52 +0200 Subject: [PATCH 07/10] chore: refresh OpenAPI spec + regenerate Pydantic models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates docs/openapi/monitoring-api.json after the upstream API change that reverted the explicit oneOf annotation on MonitorConfig — the property-level inlined oneOf preserves subtype validation in the generated models. Made-with: Cursor --- docs/openapi/monitoring-api.json | 85 +++++++++++++++++++++++--------- src/devhelm/_generated.py | 29 +++++++++-- 2 files changed, 86 insertions(+), 28 deletions(-) diff --git a/docs/openapi/monitoring-api.json b/docs/openapi/monitoring-api.json index 11940cb..d240e4b 100644 --- a/docs/openapi/monitoring-api.json +++ b/docs/openapi/monitoring-api.json @@ -22483,7 +22483,26 @@ ] }, "config": { - "$ref": "#/components/schemas/MonitorConfig" + "oneOf": [ + { + "$ref": "#/components/schemas/DnsMonitorConfig" + }, + { + "$ref": "#/components/schemas/HeartbeatMonitorConfig" + }, + { + "$ref": "#/components/schemas/HttpMonitorConfig" + }, + { + "$ref": "#/components/schemas/IcmpMonitorConfig" + }, + { + "$ref": "#/components/schemas/McpServerMonitorConfig" + }, + { + "$ref": "#/components/schemas/TcpMonitorConfig" + } + ] }, "frequencySeconds": { "type": "integer", @@ -26218,27 +26237,7 @@ }, "MonitorConfig": { "type": "object", - "description": "Protocol-specific monitor configuration; concrete type is deduced from the JSON shape", - "oneOf": [ - { - "$ref": "#/components/schemas/HttpMonitorConfig" - }, - { - "$ref": "#/components/schemas/DnsMonitorConfig" - }, - { - "$ref": "#/components/schemas/McpServerMonitorConfig" - }, - { - "$ref": "#/components/schemas/TcpMonitorConfig" - }, - { - "$ref": "#/components/schemas/IcmpMonitorConfig" - }, - { - "$ref": "#/components/schemas/HeartbeatMonitorConfig" - } - ] + "description": "Protocol-specific monitor configuration; concrete type is deduced from the JSON shape" }, "MonitorDto": { "required": [ @@ -26283,7 +26282,26 @@ ] }, "config": { - "$ref": "#/components/schemas/MonitorConfig" + "oneOf": [ + { + "$ref": "#/components/schemas/DnsMonitorConfig" + }, + { + "$ref": "#/components/schemas/HeartbeatMonitorConfig" + }, + { + "$ref": "#/components/schemas/HttpMonitorConfig" + }, + { + "$ref": "#/components/schemas/IcmpMonitorConfig" + }, + { + "$ref": "#/components/schemas/McpServerMonitorConfig" + }, + { + "$ref": "#/components/schemas/TcpMonitorConfig" + } + ] }, "frequencySeconds": { "type": "integer", @@ -26468,7 +26486,26 @@ ] }, "config": { - "$ref": "#/components/schemas/MonitorConfig" + "oneOf": [ + { + "$ref": "#/components/schemas/DnsMonitorConfig" + }, + { + "$ref": "#/components/schemas/HeartbeatMonitorConfig" + }, + { + "$ref": "#/components/schemas/HttpMonitorConfig" + }, + { + "$ref": "#/components/schemas/IcmpMonitorConfig" + }, + { + "$ref": "#/components/schemas/McpServerMonitorConfig" + }, + { + "$ref": "#/components/schemas/TcpMonitorConfig" + } + ] }, "assertions": { "type": "array", diff --git a/src/devhelm/_generated.py b/src/devhelm/_generated.py index 287fd02..cbb8dc1 100644 --- a/src/devhelm/_generated.py +++ b/src/devhelm/_generated.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: .openapi-preprocessed.json -# timestamp: 2026-04-20T10:53:24+00:00 +# timestamp: 2026-04-20T12:58:15+00:00 from __future__ import annotations from typing import Annotated, Any @@ -6231,7 +6231,14 @@ class MonitorDto(BaseModel): str, Field(description="Human-readable name for this monitor", min_length=1) ] type: Type3 - config: MonitorConfig + config: ( + DnsMonitorConfig + | HeartbeatMonitorConfig + | HttpMonitorConfig + | IcmpMonitorConfig + | McpServerMonitorConfig + | TcpMonitorConfig + ) frequency_seconds: Annotated[ int, Field( @@ -6291,7 +6298,14 @@ class MonitorDto(BaseModel): class MonitorTestRequest(BaseModel): type: Annotated[Type3, Field(description="Monitor protocol type to test")] - config: MonitorConfig + config: ( + DnsMonitorConfig + | HeartbeatMonitorConfig + | HttpMonitorConfig + | IcmpMonitorConfig + | McpServerMonitorConfig + | TcpMonitorConfig + ) assertions: Annotated[ list[CreateAssertionRequest] | None, Field(description="Optional assertions to evaluate against the test result"), @@ -6910,7 +6924,14 @@ class CreateMonitorRequest(BaseModel): ), ] type: Annotated[Type1, Field(description="Monitor protocol type")] - config: MonitorConfig + config: ( + DnsMonitorConfig + | HeartbeatMonitorConfig + | HttpMonitorConfig + | IcmpMonitorConfig + | McpServerMonitorConfig + | TcpMonitorConfig + ) frequency_seconds: Annotated[ int | None, Field( From bf9aa7f9c0dbe74ca88d9411bc6cf483a312f3dd Mon Sep 17 00:00:00 2001 From: caballeto Date: Mon, 20 Apr 2026 16:51:13 +0200 Subject: [PATCH 08/10] =?UTF-8?q?chore:=20refresh=20spec=20=E2=80=94=20Pyd?= =?UTF-8?q?antic=20discriminated=20unions=20for=20auth=20+=20check=20detai?= =?UTF-8?q?ls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picks up the upstream @devhelm/openapi-tools update that inlines discriminator subtypes and rewrites parents as oneOf+discriminator. The generated Pydantic models now use: type: Literal["bearer"] (and "basic", "header", "api_key") check_type: Literal["http"] (and "tcp", "icmp", "dns", "mcp_server") with `Annotated[Union[...], Field(discriminator="type")]` on the parent union (MonitorAuthConfig and CheckTypeDetailsDto). Pydantic's discriminator-aware union resolution now picks the right subtype in O(1) on validation, vs the previous "try every member, keep the first that parses" fallback that misclassified shape-equivalent subtypes (e.g. BearerAuthConfig vs BasicAuthConfig — both `{vault_secret_id}` only). Spec refresh also propagates the regular field-level fixes from the upstream typing audit. ruff, mypy strict, and pytest (681 tests) pass. Made-with: Cursor --- docs/openapi/monitoring-api.json | 2312 ++++++++++++++---------------- src/devhelm/_generated.py | 847 +++++++---- 2 files changed, 1631 insertions(+), 1528 deletions(-) diff --git a/docs/openapi/monitoring-api.json b/docs/openapi/monitoring-api.json index d240e4b..83a003b 100644 --- a/docs/openapi/monitoring-api.json +++ b/docs/openapi/monitoring-api.json @@ -21067,34 +21067,30 @@ }, "ApiKeyAuthConfig": { "required": [ + "type", "headerName" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorAuthConfig" - }, - { - "type": "object", - "properties": { - "headerName": { - "minLength": 1, - "pattern": "^[A-Za-z0-9\\-_]+$", - "type": "string", - "description": "HTTP header name that carries the API key" - }, - "vaultSecretId": { - "type": "string", - "description": "Vault secret ID for the API key value", - "format": "uuid", - "nullable": true - } - }, - "required": [ - "headerName" + "properties": { + "type": { + "type": "string", + "enum": [ + "api_key" ] + }, + "headerName": { + "minLength": 1, + "pattern": "^[A-Za-z0-9\\-_]+$", + "type": "string", + "description": "HTTP header name that carries the API key" + }, + "vaultSecretId": { + "type": "string", + "description": "Vault secret ID for the API key value", + "format": "uuid", + "nullable": true } - ] + } }, "ApiKeyCreateResponse": { "required": [ @@ -21190,65 +21186,6 @@ }, "description": "API key for programmatic access to the DevHelm API" }, - "AssertionConfig": { - "required": [ - "type" - ], - "type": "object", - "properties": { - "type": { - "type": "string" - } - }, - "description": "New assertion configuration (full replacement)", - "discriminator": { - "propertyName": "type", - "mapping": { - "status_code": "#/components/schemas/StatusCodeAssertion", - "response_time": "#/components/schemas/ResponseTimeAssertion", - "body_contains": "#/components/schemas/BodyContainsAssertion", - "json_path": "#/components/schemas/JsonPathAssertion", - "header_value": "#/components/schemas/HeaderValueAssertion", - "regex_body": "#/components/schemas/RegexBodyAssertion", - "dns_resolves": "#/components/schemas/DnsResolvesAssertion", - "dns_response_time": "#/components/schemas/DnsResponseTimeAssertion", - "dns_expected_ips": "#/components/schemas/DnsExpectedIpsAssertion", - "dns_expected_cname": "#/components/schemas/DnsExpectedCnameAssertion", - "dns_record_contains": "#/components/schemas/DnsRecordContainsAssertion", - "dns_record_equals": "#/components/schemas/DnsRecordEqualsAssertion", - "dns_txt_contains": "#/components/schemas/DnsTxtContainsAssertion", - "dns_min_answers": "#/components/schemas/DnsMinAnswersAssertion", - "dns_max_answers": "#/components/schemas/DnsMaxAnswersAssertion", - "dns_response_time_warn": "#/components/schemas/DnsResponseTimeWarnAssertion", - "dns_ttl_low": "#/components/schemas/DnsTtlLowAssertion", - "dns_ttl_high": "#/components/schemas/DnsTtlHighAssertion", - "mcp_connects": "#/components/schemas/McpConnectsAssertion", - "mcp_response_time": "#/components/schemas/McpResponseTimeAssertion", - "mcp_has_capability": "#/components/schemas/McpHasCapabilityAssertion", - "mcp_tool_available": "#/components/schemas/McpToolAvailableAssertion", - "mcp_min_tools": "#/components/schemas/McpMinToolsAssertion", - "mcp_protocol_version": "#/components/schemas/McpProtocolVersionAssertion", - "mcp_response_time_warn": "#/components/schemas/McpResponseTimeWarnAssertion", - "mcp_tool_count_changed": "#/components/schemas/McpToolCountChangedAssertion", - "ssl_expiry": "#/components/schemas/SslExpiryAssertion", - "response_size": "#/components/schemas/ResponseSizeAssertion", - "redirect_count": "#/components/schemas/RedirectCountAssertion", - "redirect_target": "#/components/schemas/RedirectTargetAssertion", - "response_time_warn": "#/components/schemas/ResponseTimeWarnAssertion", - "tcp_connects": "#/components/schemas/TcpConnectsAssertion", - "tcp_response_time": "#/components/schemas/TcpResponseTimeAssertion", - "tcp_response_time_warn": "#/components/schemas/TcpResponseTimeWarnAssertion", - "icmp_reachable": "#/components/schemas/IcmpReachableAssertion", - "icmp_response_time": "#/components/schemas/IcmpResponseTimeAssertion", - "icmp_response_time_warn": "#/components/schemas/IcmpResponseTimeWarnAssertion", - "icmp_packet_loss": "#/components/schemas/IcmpPacketLossAssertion", - "heartbeat_received": "#/components/schemas/HeartbeatReceivedAssertion", - "heartbeat_max_interval": "#/components/schemas/HeartbeatMaxIntervalAssertion", - "heartbeat_interval_drift": "#/components/schemas/HeartbeatIntervalDriftAssertion", - "heartbeat_payload_contains": "#/components/schemas/HeartbeatPayloadContainsAssertion" - } - } - }, "AssertionResultDto": { "required": [ "severity", @@ -21465,21 +21402,22 @@ }, "BasicAuthConfig": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorAuthConfig" + "properties": { + "type": { + "type": "string", + "enum": [ + "basic" + ] }, - { - "type": "object", - "properties": { - "vaultSecretId": { - "type": "string", - "description": "Vault secret ID holding Basic auth username and password", - "format": "uuid", - "nullable": true - } - } + "vaultSecretId": { + "type": "string", + "description": "Vault secret ID holding Basic auth username and password", + "format": "uuid", + "nullable": true } + }, + "required": [ + "type" ] }, "BatchComponentUptimeDto": { @@ -21504,46 +21442,43 @@ }, "BearerAuthConfig": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorAuthConfig" + "properties": { + "type": { + "type": "string", + "enum": [ + "bearer" + ] }, - { - "type": "object", - "properties": { - "vaultSecretId": { - "type": "string", - "description": "Vault secret ID holding the bearer token value", - "format": "uuid", - "nullable": true - } - } + "vaultSecretId": { + "type": "string", + "description": "Vault secret ID holding the bearer token value", + "format": "uuid", + "nullable": true } + }, + "required": [ + "type" ] }, "BodyContainsAssertion": { "required": [ + "type", "substring" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "substring": { - "minLength": 1, - "type": "string", - "description": "Substring that must appear in the response body" - } - }, - "required": [ - "substring" + "properties": { + "type": { + "type": "string", + "enum": [ + "body_contains" ] + }, + "substring": { + "minLength": 1, + "type": "string", + "description": "Substring that must appear in the response body" } - ] + } }, "BulkMonitorActionRequest": { "required": [ @@ -21679,30 +21614,6 @@ }, "description": "Update an organization member's status" }, - "ChannelConfig": { - "required": [ - "channelType" - ], - "type": "object", - "properties": { - "channelType": { - "type": "string" - } - }, - "description": "New channel configuration (full replacement, not partial update)", - "discriminator": { - "propertyName": "channelType", - "mapping": { - "email": "#/components/schemas/EmailChannelConfig", - "slack": "#/components/schemas/SlackChannelConfig", - "webhook": "#/components/schemas/WebhookChannelConfig", - "pagerduty": "#/components/schemas/PagerDutyChannelConfig", - "opsgenie": "#/components/schemas/OpsGenieChannelConfig", - "teams": "#/components/schemas/TeamsChannelConfig", - "discord": "#/components/schemas/DiscordChannelConfig" - } - } - }, "ChartBucketDto": { "required": [ "bucket" @@ -21886,15 +21797,6 @@ "description": "A single check result from a monitor run" }, "CheckTypeDetailsDto": { - "required": [ - "check_type" - ], - "type": "object", - "properties": { - "check_type": { - "type": "string" - } - }, "description": "Check-type-specific details — polymorphic by check_type discriminator", "discriminator": { "propertyName": "check_type", @@ -21905,7 +21807,24 @@ "dns": "#/components/schemas/Dns", "mcp_server": "#/components/schemas/McpServer" } - } + }, + "oneOf": [ + { + "$ref": "#/components/schemas/Http" + }, + { + "$ref": "#/components/schemas/Tcp" + }, + { + "$ref": "#/components/schemas/Icmp" + }, + { + "$ref": "#/components/schemas/Dns" + }, + { + "$ref": "#/components/schemas/McpServer" + } + ] }, "ComponentImpact": { "required": [ @@ -23425,218 +23344,199 @@ }, "DiscordChannelConfig": { "required": [ + "channelType", "webhookUrl" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/ChannelConfig" - }, - { - "type": "object", - "properties": { - "webhookUrl": { - "minLength": 1, - "type": "string", - "description": "Discord webhook URL" - }, - "mentionRoleId": { - "type": "string", - "description": "Optional Discord role ID to mention in notifications", - "nullable": true - } - }, - "required": [ - "webhookUrl" + "properties": { + "channelType": { + "type": "string", + "enum": [ + "discord" ] + }, + "webhookUrl": { + "minLength": 1, + "type": "string", + "description": "Discord webhook URL" + }, + "mentionRoleId": { + "type": "string", + "description": "Optional Discord role ID to mention in notifications", + "nullable": true } - ] + } }, "Dns": { "type": "object", "description": "DNS check-type-specific details", - "allOf": [ - { - "$ref": "#/components/schemas/CheckTypeDetailsDto" + "properties": { + "check_type": { + "type": "string", + "enum": [ + "dns" + ] }, - { + "hostname": { + "type": "string", + "description": "Target hostname", + "nullable": true + }, + "requestedTypes": { + "type": "array", + "description": "Requested DNS record types", + "nullable": true, + "items": { + "type": "string", + "description": "Requested DNS record types", + "nullable": true + } + }, + "usedResolver": { + "type": "string", + "description": "Resolver used for lookup", + "nullable": true + }, + "records": { "type": "object", - "properties": { - "hostname": { - "type": "string", - "description": "Target hostname", - "nullable": true - }, - "requestedTypes": { - "type": "array", - "description": "Requested DNS record types", - "nullable": true, - "items": { - "type": "string", - "description": "Requested DNS record types", - "nullable": true - } - }, - "usedResolver": { - "type": "string", - "description": "Resolver used for lookup", - "nullable": true - }, - "records": { + "additionalProperties": { + "type": "array", + "description": "Resolved DNS records keyed by record type", + "nullable": true, + "items": { "type": "object", "additionalProperties": { - "type": "array", + "type": "object", "description": "Resolved DNS records keyed by record type", - "nullable": true, - "items": { - "type": "object", - "additionalProperties": { - "type": "object", - "description": "Resolved DNS records keyed by record type", - "nullable": true - }, - "description": "Resolved DNS records keyed by record type", - "nullable": true - } + "nullable": true }, "description": "Resolved DNS records keyed by record type", "nullable": true - }, - "attempts": { - "type": "array", + } + }, + "description": "Resolved DNS records keyed by record type", + "nullable": true + }, + "attempts": { + "type": "array", + "description": "DNS resolution attempts", + "nullable": true, + "items": { + "type": "object", + "additionalProperties": { + "type": "object", "description": "DNS resolution attempts", - "nullable": true, - "items": { - "type": "object", - "additionalProperties": { - "type": "object", - "description": "DNS resolution attempts", - "nullable": true - }, - "description": "DNS resolution attempts", - "nullable": true - } - }, - "failureKind": { - "type": "string", - "description": "Kind of DNS failure, if any", "nullable": true - } + }, + "description": "DNS resolution attempts", + "nullable": true } + }, + "failureKind": { + "type": "string", + "description": "Kind of DNS failure, if any", + "nullable": true } + }, + "required": [ + "check_type" ] }, "DnsExpectedCnameAssertion": { "required": [ + "type", "value" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "value": { - "minLength": 1, - "type": "string", - "description": "Expected CNAME target the resolution must include" - } - }, - "required": [ - "value" + "properties": { + "type": { + "type": "string", + "enum": [ + "dns_expected_cname" ] + }, + "value": { + "minLength": 1, + "type": "string", + "description": "Expected CNAME target the resolution must include" } - ] + } }, "DnsExpectedIpsAssertion": { "required": [ + "type", "ips" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "ips": { - "minItems": 1, - "type": "array", - "description": "Allowed IP addresses; at least one resolved address must match", - "items": { - "type": "string", - "description": "Allowed IP addresses; at least one resolved address must match" - } - } - }, - "required": [ - "ips" + "properties": { + "type": { + "type": "string", + "enum": [ + "dns_expected_ips" ] + }, + "ips": { + "minItems": 1, + "type": "array", + "description": "Allowed IP addresses; at least one resolved address must match", + "items": { + "type": "string", + "description": "Allowed IP addresses; at least one resolved address must match" + } } - ] + } }, "DnsMaxAnswersAssertion": { "required": [ - "recordType" + "type", + "recordType", + "max" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "recordType": { - "minLength": 1, - "type": "string", - "description": "DNS record type whose answer count is checked" - }, - "max": { - "type": "integer", - "description": "Maximum number of answers allowed for that record type", - "format": "int32" - } - }, - "required": [ - "recordType", - "max" + "properties": { + "type": { + "type": "string", + "enum": [ + "dns_max_answers" ] + }, + "recordType": { + "minLength": 1, + "type": "string", + "description": "DNS record type whose answer count is checked" + }, + "max": { + "type": "integer", + "description": "Maximum number of answers allowed for that record type", + "format": "int32" } - ] + } }, "DnsMinAnswersAssertion": { "required": [ - "recordType" + "type", + "recordType", + "min" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "recordType": { - "minLength": 1, - "type": "string", - "description": "DNS record type whose answer count is checked" - }, - "min": { - "type": "integer", - "description": "Minimum number of answers required for that record type", - "format": "int32" - } - }, - "required": [ - "recordType", - "min" + "properties": { + "type": { + "type": "string", + "enum": [ + "dns_min_answers" ] + }, + "recordType": { + "minLength": 1, + "type": "string", + "description": "DNS record type whose answer count is checked" + }, + "min": { + "type": "integer", + "description": "Minimum number of answers required for that record type", + "format": "int32" } - ] + } }, "DnsMonitorConfig": { "required": [ @@ -23708,210 +23608,194 @@ }, "DnsRecordContainsAssertion": { "required": [ + "type", "recordType", "substring" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "recordType": { - "minLength": 1, - "type": "string", - "description": "DNS record type to assert on (A, AAAA, CNAME, MX, TXT)" - }, - "substring": { - "minLength": 1, - "type": "string", - "description": "Substring that must appear in a matching record value" - } - }, - "required": [ - "recordType", - "substring" + "properties": { + "type": { + "type": "string", + "enum": [ + "dns_record_contains" ] + }, + "recordType": { + "minLength": 1, + "type": "string", + "description": "DNS record type to assert on (A, AAAA, CNAME, MX, TXT)" + }, + "substring": { + "minLength": 1, + "type": "string", + "description": "Substring that must appear in a matching record value" } - ] + } }, "DnsRecordEqualsAssertion": { "required": [ + "type", "recordType", "value" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "recordType": { - "minLength": 1, - "type": "string", - "description": "DNS record type to assert on (A, AAAA, CNAME, MX, TXT)" - }, - "value": { - "minLength": 1, - "type": "string", - "description": "Expected DNS record value for an exact match" - } - }, - "required": [ - "recordType", - "value" + "properties": { + "type": { + "type": "string", + "enum": [ + "dns_record_equals" ] + }, + "recordType": { + "minLength": 1, + "type": "string", + "description": "DNS record type to assert on (A, AAAA, CNAME, MX, TXT)" + }, + "value": { + "minLength": 1, + "type": "string", + "description": "Expected DNS record value for an exact match" } - ] + } }, "DnsResolvesAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" + "properties": { + "type": { + "type": "string", + "enum": [ + "dns_resolves" + ] } + }, + "required": [ + "type" ] }, "DnsResponseTimeAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxMs": { - "type": "integer", - "description": "Maximum allowed DNS resolution time in milliseconds", - "format": "int32" - } - }, - "required": [ - "maxMs" + "properties": { + "type": { + "type": "string", + "enum": [ + "dns_response_time" ] + }, + "maxMs": { + "type": "integer", + "description": "Maximum allowed DNS resolution time in milliseconds", + "format": "int32" } + }, + "required": [ + "type", + "maxMs" ] }, "DnsResponseTimeWarnAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "warnMs": { - "type": "integer", - "description": "DNS resolution time in milliseconds that triggers a warning only", - "format": "int32" - } - }, - "required": [ - "warnMs" + "properties": { + "type": { + "type": "string", + "enum": [ + "dns_response_time_warn" ] + }, + "warnMs": { + "type": "integer", + "description": "DNS resolution time in milliseconds that triggers a warning only", + "format": "int32" } + }, + "required": [ + "type", + "warnMs" ] }, "DnsTtlHighAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxTtl": { - "type": "integer", - "description": "Maximum TTL in seconds before a high-TTL warning is raised", - "format": "int32" - } - }, - "required": [ - "maxTtl" + "properties": { + "type": { + "type": "string", + "enum": [ + "dns_ttl_high" ] + }, + "maxTtl": { + "type": "integer", + "description": "Maximum TTL in seconds before a high-TTL warning is raised", + "format": "int32" } + }, + "required": [ + "type", + "maxTtl" ] }, "DnsTtlLowAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "minTtl": { - "type": "integer", - "description": "Minimum acceptable TTL in seconds before a warning is raised", - "format": "int32" - } - }, - "required": [ - "minTtl" + "properties": { + "type": { + "type": "string", + "enum": [ + "dns_ttl_low" ] + }, + "minTtl": { + "type": "integer", + "description": "Minimum acceptable TTL in seconds before a warning is raised", + "format": "int32" } + }, + "required": [ + "type", + "minTtl" ] }, "DnsTxtContainsAssertion": { "required": [ + "type", "substring" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "substring": { - "minLength": 1, - "type": "string", - "description": "Substring that must appear in at least one TXT record" - } - }, - "required": [ - "substring" + "properties": { + "type": { + "type": "string", + "enum": [ + "dns_txt_contains" ] + }, + "substring": { + "minLength": 1, + "type": "string", + "description": "Substring that must appear in at least one TXT record" } - ] + } }, "EmailChannelConfig": { "required": [ + "channelType", "recipients" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/ChannelConfig" - }, - { - "type": "object", - "properties": { - "recipients": { - "minItems": 1, - "type": "array", - "description": "Email addresses to send notifications to", - "items": { - "type": "string", - "description": "Email addresses to send notifications to", - "format": "email" - } - } - }, - "required": [ - "recipients" + "properties": { + "channelType": { + "type": "string", + "enum": [ + "email" ] + }, + "recipients": { + "minItems": 1, + "type": "array", + "description": "Email addresses to send notifications to", + "items": { + "type": "string", + "description": "Email addresses to send notifications to", + "format": "email" + } } - ] + } }, "EntitlementDto": { "required": [ @@ -24206,137 +24090,119 @@ "description": "Ordered component IDs with their within-group display order", "items": { "$ref": "#/components/schemas/ComponentPosition" - } - } - }, - "description": "Component ordering within a single group" - }, - "HeaderAuthConfig": { - "required": [ - "headerName" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorAuthConfig" - }, - { - "type": "object", - "properties": { - "headerName": { - "minLength": 1, - "pattern": "^[A-Za-z0-9\\-_]+$", - "type": "string", - "description": "Custom HTTP header name for the secret value" - }, - "vaultSecretId": { - "type": "string", - "description": "Vault secret ID for the header value", - "format": "uuid", - "nullable": true - } - }, - "required": [ - "headerName" + } + } + }, + "description": "Component ordering within a single group" + }, + "HeaderAuthConfig": { + "required": [ + "type", + "headerName" + ], + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "header" ] + }, + "headerName": { + "minLength": 1, + "pattern": "^[A-Za-z0-9\\-_]+$", + "type": "string", + "description": "Custom HTTP header name for the secret value" + }, + "vaultSecretId": { + "type": "string", + "description": "Vault secret ID for the header value", + "format": "uuid", + "nullable": true } - ] + } }, "HeaderValueAssertion": { "required": [ - "expected", + "type", "headerName", + "expected", "operator" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" + "properties": { + "type": { + "type": "string", + "enum": [ + "header_value" + ] }, - { - "type": "object", - "properties": { - "headerName": { - "minLength": 1, - "type": "string", - "description": "HTTP header name to assert on" - }, - "expected": { - "minLength": 1, - "type": "string", - "description": "Expected value to compare against" - }, - "operator": { - "type": "string", - "description": "Comparison operator (equals, contains, less_than, greater_than, etc.)", - "enum": [ - "equals", - "contains", - "less_than", - "greater_than", - "matches", - "range" - ] - } - }, - "required": [ - "headerName", - "expected", - "operator" + "headerName": { + "minLength": 1, + "type": "string", + "description": "HTTP header name to assert on" + }, + "expected": { + "minLength": 1, + "type": "string", + "description": "Expected value to compare against" + }, + "operator": { + "type": "string", + "description": "Comparison operator (equals, contains, less_than, greater_than, etc.)", + "enum": [ + "equals", + "contains", + "less_than", + "greater_than", + "matches", + "range" ] } - ] + } }, "HeartbeatIntervalDriftAssertion": { "required": [ + "type", "maxDeviationPercent" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxDeviationPercent": { - "maximum": 100, - "minimum": 1, - "type": "integer", - "description": "Max percent drift from expected ping interval before warning (non-fatal)", - "format": "int32" - } - }, - "required": [ - "maxDeviationPercent" + "properties": { + "type": { + "type": "string", + "enum": [ + "heartbeat_interval_drift" ] + }, + "maxDeviationPercent": { + "maximum": 100, + "minimum": 1, + "type": "integer", + "description": "Max percent drift from expected ping interval before warning (non-fatal)", + "format": "int32" } - ] + } }, "HeartbeatMaxIntervalAssertion": { "required": [ + "type", "maxSeconds" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxSeconds": { - "minimum": 1, - "type": "integer", - "description": "Maximum allowed gap in seconds between consecutive heartbeat pings", - "format": "int32" - } - }, - "required": [ - "maxSeconds" + "properties": { + "type": { + "type": "string", + "enum": [ + "heartbeat_max_interval" ] + }, + "maxSeconds": { + "minimum": 1, + "type": "integer", + "description": "Maximum allowed gap in seconds between consecutive heartbeat pings", + "format": "int32" } - ] + } }, "HeartbeatMonitorConfig": { "required": [ @@ -24374,33 +24240,28 @@ }, "HeartbeatPayloadContainsAssertion": { "required": [ + "type", "path", "value" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "path": { - "minLength": 1, - "type": "string", - "description": "JSONPath expression into the heartbeat ping JSON payload" - }, - "value": { - "type": "string", - "description": "Expected value to compare against at that path" - } - }, - "required": [ - "path", - "value" + "properties": { + "type": { + "type": "string", + "enum": [ + "heartbeat_payload_contains" ] + }, + "path": { + "minLength": 1, + "type": "string", + "description": "JSONPath expression into the heartbeat ping JSON payload" + }, + "value": { + "type": "string", + "description": "Expected value to compare against at that path" } - ] + } }, "HeartbeatPingResponse": { "required": [ @@ -24418,39 +24279,46 @@ }, "HeartbeatReceivedAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" + "properties": { + "type": { + "type": "string", + "enum": [ + "heartbeat_received" + ] } + }, + "required": [ + "type" ] }, "Http": { "type": "object", "description": "HTTP check-type-specific details", - "allOf": [ - { - "$ref": "#/components/schemas/CheckTypeDetailsDto" + "properties": { + "check_type": { + "type": "string", + "enum": [ + "http" + ] }, - { + "timing": { "type": "object", - "properties": { - "timing": { - "type": "object", - "additionalProperties": { - "type": "object", - "description": "Request phase timing breakdown", - "nullable": true - }, - "description": "Request phase timing breakdown", - "nullable": true - }, - "bodyTruncated": { - "type": "boolean", - "description": "Whether the response body was truncated before storage", - "nullable": true - } - } + "additionalProperties": { + "type": "object", + "description": "Request phase timing breakdown", + "nullable": true + }, + "description": "Request phase timing breakdown", + "nullable": true + }, + "bodyTruncated": { + "type": "boolean", + "description": "Whether the response body was truncated before storage", + "nullable": true } + }, + "required": [ + "check_type" ] }, "HttpMonitorConfig": { @@ -24518,71 +24386,67 @@ }, "Icmp": { "required": [ + "check_type", "host" ], "type": "object", "description": "ICMP (ping) check-type-specific details", - "allOf": [ - { - "$ref": "#/components/schemas/CheckTypeDetailsDto" - }, - { - "type": "object", - "properties": { - "host": { - "type": "string", - "description": "Target host", - "example": "1.1.1.1" - }, - "packetsSent": { - "type": "integer", - "description": "Number of ICMP packets sent", - "format": "int32", - "nullable": true - }, - "packetsReceived": { - "type": "integer", - "description": "Number of ICMP packets received", - "format": "int32", - "nullable": true - }, - "packetLoss": { - "type": "number", - "description": "Packet loss percentage", - "format": "double", - "nullable": true, - "example": 0 - }, - "avgRttMs": { - "type": "number", - "description": "Average round-trip time in ms", - "format": "double", - "nullable": true - }, - "minRttMs": { - "type": "number", - "description": "Minimum round-trip time in ms", - "format": "double", - "nullable": true - }, - "maxRttMs": { - "type": "number", - "description": "Maximum round-trip time in ms", - "format": "double", - "nullable": true - }, - "jitterMs": { - "type": "number", - "description": "Jitter in ms", - "format": "double", - "nullable": true - } - }, - "required": [ - "host" + "properties": { + "check_type": { + "type": "string", + "enum": [ + "icmp" ] + }, + "host": { + "type": "string", + "description": "Target host", + "example": "1.1.1.1" + }, + "packetsSent": { + "type": "integer", + "description": "Number of ICMP packets sent", + "format": "int32", + "nullable": true + }, + "packetsReceived": { + "type": "integer", + "description": "Number of ICMP packets received", + "format": "int32", + "nullable": true + }, + "packetLoss": { + "type": "number", + "description": "Packet loss percentage", + "format": "double", + "nullable": true, + "example": 0 + }, + "avgRttMs": { + "type": "number", + "description": "Average round-trip time in ms", + "format": "double", + "nullable": true + }, + "minRttMs": { + "type": "number", + "description": "Minimum round-trip time in ms", + "format": "double", + "nullable": true + }, + "maxRttMs": { + "type": "number", + "description": "Maximum round-trip time in ms", + "format": "double", + "nullable": true + }, + "jitterMs": { + "type": "number", + "description": "Jitter in ms", + "format": "double", + "nullable": true } - ] + } }, "IcmpMonitorConfig": { "required": [ @@ -24619,82 +24483,85 @@ "required": [ "host" ] - } - ] - }, - "IcmpPacketLossAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxPercent": { - "maximum": 100, - "exclusiveMaximum": false, - "minimum": 0, - "exclusiveMinimum": false, - "type": "number", - "description": "Maximum allowed packet loss percentage before the check fails (0–100)", - "format": "double" - } - }, - "required": [ - "maxPercent" + } + ] + }, + "IcmpPacketLossAssertion": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "icmp_packet_loss" ] + }, + "maxPercent": { + "maximum": 100, + "exclusiveMaximum": false, + "minimum": 0, + "exclusiveMinimum": false, + "type": "number", + "description": "Maximum allowed packet loss percentage before the check fails (0–100)", + "format": "double" } + }, + "required": [ + "type", + "maxPercent" ] }, "IcmpReachableAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" + "properties": { + "type": { + "type": "string", + "enum": [ + "icmp_reachable" + ] } + }, + "required": [ + "type" ] }, "IcmpResponseTimeAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxMs": { - "type": "integer", - "description": "Maximum average ICMP round-trip time in milliseconds", - "format": "int32" - } - }, - "required": [ - "maxMs" + "properties": { + "type": { + "type": "string", + "enum": [ + "icmp_response_time" ] + }, + "maxMs": { + "type": "integer", + "description": "Maximum average ICMP round-trip time in milliseconds", + "format": "int32" } + }, + "required": [ + "type", + "maxMs" ] }, "IcmpResponseTimeWarnAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "warnMs": { - "type": "integer", - "description": "ICMP round-trip time in milliseconds that triggers a warning only", - "format": "int32" - } - }, - "required": [ - "warnMs" + "properties": { + "type": { + "type": "string", + "enum": [ + "icmp_response_time_warn" ] + }, + "warnMs": { + "type": "integer", + "description": "ICMP round-trip time in milliseconds that triggers a warning only", + "format": "int32" } + }, + "required": [ + "type", + "warnMs" ] }, "IncidentDetailDto": { @@ -25356,48 +25223,42 @@ }, "JsonPathAssertion": { "required": [ + "type", + "path", "expected", - "operator", - "path" + "operator" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" + "properties": { + "type": { + "type": "string", + "enum": [ + "json_path" + ] }, - { - "type": "object", - "properties": { - "path": { - "minLength": 1, - "type": "string", - "description": "JSONPath expression to extract a value from the response body" - }, - "expected": { - "minLength": 1, - "type": "string", - "description": "Expected value to compare against" - }, - "operator": { - "type": "string", - "description": "Comparison operator (equals, contains, less_than, greater_than, etc.)", - "enum": [ - "equals", - "contains", - "less_than", - "greater_than", - "matches", - "range" - ] - } - }, - "required": [ - "path", - "expected", - "operator" + "path": { + "minLength": 1, + "type": "string", + "description": "JSONPath expression to extract a value from the response body" + }, + "expected": { + "minLength": 1, + "type": "string", + "description": "Expected value to compare against" + }, + "operator": { + "type": "string", + "description": "Comparison operator (equals, contains, less_than, greater_than, etc.)", + "enum": [ + "equals", + "contains", + "less_than", + "greater_than", + "matches", + "range" ] } - ] + } }, "KeyInfo": { "required": [ @@ -25655,173 +25516,169 @@ }, "McpConnectsAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" + "properties": { + "type": { + "type": "string", + "enum": [ + "mcp_connects" + ] } + }, + "required": [ + "type" ] }, "McpHasCapabilityAssertion": { "required": [ + "type", "capability" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "capability": { - "minLength": 1, - "type": "string", - "description": "Capability name the server must advertise, e.g. tools or resources" - } - }, - "required": [ - "capability" + "properties": { + "type": { + "type": "string", + "enum": [ + "mcp_has_capability" ] + }, + "capability": { + "minLength": 1, + "type": "string", + "description": "Capability name the server must advertise, e.g. tools or resources" } - ] + } }, "McpMinToolsAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "min": { - "type": "integer", - "description": "Minimum number of tools the server must expose", - "format": "int32" - } - }, - "required": [ - "min" + "properties": { + "type": { + "type": "string", + "enum": [ + "mcp_min_tools" ] + }, + "min": { + "type": "integer", + "description": "Minimum number of tools the server must expose", + "format": "int32" } + }, + "required": [ + "type", + "min" ] }, "McpProtocolVersionAssertion": { "required": [ + "type", "version" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "version": { - "minLength": 1, - "type": "string", - "description": "Expected MCP protocol version string from the server handshake" - } - }, - "required": [ - "version" + "properties": { + "type": { + "type": "string", + "enum": [ + "mcp_protocol_version" ] + }, + "version": { + "minLength": 1, + "type": "string", + "description": "Expected MCP protocol version string from the server handshake" } - ] + } }, "McpResponseTimeAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxMs": { - "type": "integer", - "description": "Maximum allowed MCP check duration in milliseconds", - "format": "int32" - } - }, - "required": [ - "maxMs" + "properties": { + "type": { + "type": "string", + "enum": [ + "mcp_response_time" ] + }, + "maxMs": { + "type": "integer", + "description": "Maximum allowed MCP check duration in milliseconds", + "format": "int32" } + }, + "required": [ + "type", + "maxMs" ] }, "McpResponseTimeWarnAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "warnMs": { - "type": "integer", - "description": "MCP check duration in milliseconds that triggers a warning only", - "format": "int32" - } - }, - "required": [ - "warnMs" + "properties": { + "type": { + "type": "string", + "enum": [ + "mcp_response_time_warn" ] + }, + "warnMs": { + "type": "integer", + "description": "MCP check duration in milliseconds that triggers a warning only", + "format": "int32" } + }, + "required": [ + "type", + "warnMs" ] }, "McpServer": { "type": "object", "description": "MCP server check-type-specific details", - "allOf": [ - { - "$ref": "#/components/schemas/CheckTypeDetailsDto" - }, - { - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "MCP server URL", - "nullable": true - }, - "protocolVersion": { - "type": "string", - "description": "MCP protocol version", - "nullable": true - }, - "serverInfo": { - "type": "object", - "additionalProperties": { - "type": "object", - "description": "MCP server info (name, version, etc.)", - "nullable": true - }, - "description": "MCP server info (name, version, etc.)", - "nullable": true - }, - "toolCount": { - "type": "integer", - "description": "Number of tools exposed", - "format": "int32", - "nullable": true - }, - "resourceCount": { - "type": "integer", - "description": "Number of resources exposed", - "format": "int32", - "nullable": true - }, - "promptCount": { - "type": "integer", - "description": "Number of prompts exposed", - "format": "int32", - "nullable": true - } - } + "properties": { + "check_type": { + "type": "string", + "enum": [ + "mcp_server" + ] + }, + "url": { + "type": "string", + "description": "MCP server URL", + "nullable": true + }, + "protocolVersion": { + "type": "string", + "description": "MCP protocol version", + "nullable": true + }, + "serverInfo": { + "type": "object", + "additionalProperties": { + "type": "object", + "description": "MCP server info (name, version, etc.)", + "nullable": true + }, + "description": "MCP server info (name, version, etc.)", + "nullable": true + }, + "toolCount": { + "type": "integer", + "description": "Number of tools exposed", + "format": "int32", + "nullable": true + }, + "resourceCount": { + "type": "integer", + "description": "Number of resources exposed", + "format": "int32", + "nullable": true + }, + "promptCount": { + "type": "integer", + "description": "Number of prompts exposed", + "format": "int32", + "nullable": true } + }, + "required": [ + "check_type" ] }, "McpServerMonitorConfig": { @@ -25870,47 +25727,42 @@ }, "McpToolAvailableAssertion": { "required": [ + "type", "toolName" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "toolName": { - "minLength": 1, - "type": "string", - "description": "MCP tool name that must appear in the server's tool list" - } - }, - "required": [ - "toolName" + "properties": { + "type": { + "type": "string", + "enum": [ + "mcp_tool_available" ] + }, + "toolName": { + "minLength": 1, + "type": "string", + "description": "MCP tool name that must appear in the server's tool list" } - ] + } }, "McpToolCountChangedAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "expectedCount": { - "type": "integer", - "description": "Expected tool count; warns when the live count differs", - "format": "int32" - } - }, - "required": [ - "expectedCount" + "properties": { + "type": { + "type": "string", + "enum": [ + "mcp_tool_count_changed" ] + }, + "expectedCount": { + "type": "integer", + "description": "Expected tool count; warns when the live count differs", + "format": "int32" } + }, + "required": [ + "type", + "expectedCount" ] }, "MemberDto": { @@ -26171,15 +26023,6 @@ } }, "MonitorAuthConfig": { - "required": [ - "type" - ], - "type": "object", - "properties": { - "type": { - "type": "string" - } - }, "description": "New authentication configuration (full replacement)", "discriminator": { "propertyName": "type", @@ -26189,7 +26032,21 @@ "header": "#/components/schemas/HeaderAuthConfig", "api_key": "#/components/schemas/ApiKeyAuthConfig" } - } + }, + "oneOf": [ + { + "$ref": "#/components/schemas/BearerAuthConfig" + }, + { + "$ref": "#/components/schemas/BasicAuthConfig" + }, + { + "$ref": "#/components/schemas/HeaderAuthConfig" + }, + { + "$ref": "#/components/schemas/ApiKeyAuthConfig" + } + ] }, "MonitorAuthDto": { "required": [ @@ -26882,32 +26739,28 @@ }, "OpsGenieChannelConfig": { "required": [ + "channelType", "apiKey" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/ChannelConfig" - }, - { - "type": "object", - "properties": { - "apiKey": { - "minLength": 1, - "type": "string", - "description": "OpsGenie API key for alert creation" - }, - "region": { - "type": "string", - "description": "OpsGenie API region: us or eu", - "nullable": true - } - }, - "required": [ - "apiKey" + "properties": { + "channelType": { + "type": "string", + "enum": [ + "opsgenie" ] + }, + "apiKey": { + "minLength": 1, + "type": "string", + "description": "OpsGenie API key for alert creation" + }, + "region": { + "type": "string", + "description": "OpsGenie API region: us or eu", + "nullable": true } - ] + } }, "OrganizationDto": { "required": [ @@ -26996,32 +26849,28 @@ }, "PagerDutyChannelConfig": { "required": [ + "channelType", "routingKey" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/ChannelConfig" - }, - { - "type": "object", - "properties": { - "routingKey": { - "minLength": 1, - "type": "string", - "description": "PagerDuty Events API v2 routing (integration) key" - }, - "severityOverride": { - "type": "string", - "description": "Override PagerDuty severity mapping", - "nullable": true - } - }, - "required": [ - "routingKey" + "properties": { + "channelType": { + "type": "string", + "enum": [ + "pagerduty" ] + }, + "routingKey": { + "minLength": 1, + "type": "string", + "description": "PagerDuty Events API v2 routing (integration) key" + }, + "severityOverride": { + "type": "string", + "description": "Override PagerDuty severity mapping", + "nullable": true } - ] + } }, "PageSection": { "type": "object", @@ -27245,86 +27094,76 @@ }, "RedirectCountAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxCount": { - "type": "integer", - "description": "Maximum number of HTTP redirects allowed before the check fails", - "format": "int32" - } - }, - "required": [ - "maxCount" + "properties": { + "type": { + "type": "string", + "enum": [ + "redirect_count" ] + }, + "maxCount": { + "type": "integer", + "description": "Maximum number of HTTP redirects allowed before the check fails", + "format": "int32" } + }, + "required": [ + "type", + "maxCount" ] }, "RedirectTargetAssertion": { "required": [ + "type", "expected", "operator" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" + "properties": { + "type": { + "type": "string", + "enum": [ + "redirect_target" + ] }, - { - "type": "object", - "properties": { - "expected": { - "minLength": 1, - "type": "string", - "description": "Expected final URL after following redirects" - }, - "operator": { - "type": "string", - "description": "Comparison operator (equals, contains, less_than, greater_than, etc.)", - "enum": [ - "equals", - "contains", - "less_than", - "greater_than", - "matches", - "range" - ] - } - }, - "required": [ - "expected", - "operator" + "expected": { + "minLength": 1, + "type": "string", + "description": "Expected final URL after following redirects" + }, + "operator": { + "type": "string", + "description": "Comparison operator (equals, contains, less_than, greater_than, etc.)", + "enum": [ + "equals", + "contains", + "less_than", + "greater_than", + "matches", + "range" ] } - ] + } }, "RegexBodyAssertion": { "required": [ + "type", "pattern" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "pattern": { - "minLength": 1, - "type": "string", - "description": "Regular expression the response body must match" - } - }, - "required": [ - "pattern" + "properties": { + "type": { + "type": "string", + "enum": [ + "regex_body" ] + }, + "pattern": { + "minLength": 1, + "type": "string", + "description": "Regular expression the response body must match" } - ] + } }, "RegionStatusDto": { "required": [ @@ -27746,65 +27585,62 @@ }, "ResponseSizeAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxBytes": { - "type": "integer", - "description": "Maximum response body size in bytes before the check fails", - "format": "int32" - } - }, - "required": [ - "maxBytes" + "properties": { + "type": { + "type": "string", + "enum": [ + "response_size" ] + }, + "maxBytes": { + "type": "integer", + "description": "Maximum response body size in bytes before the check fails", + "format": "int32" } + }, + "required": [ + "type", + "maxBytes" ] }, "ResponseTimeAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "thresholdMs": { - "type": "integer", - "description": "Maximum allowed response time in milliseconds before the check fails", - "format": "int32" - } - }, - "required": [ - "thresholdMs" + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "response_time" ] + }, + "thresholdMs": { + "type": "integer", + "description": "Maximum allowed response time in milliseconds before the check fails", + "format": "int32" } + }, + "required": [ + "type", + "thresholdMs" ] }, "ResponseTimeWarnAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "warnMs": { - "type": "integer", - "description": "HTTP response time in milliseconds that triggers a warning only", - "format": "int32" - } - }, - "required": [ - "warnMs" + "properties": { + "type": { + "type": "string", + "enum": [ + "response_time_warn" ] + }, + "warnMs": { + "type": "integer", + "description": "HTTP response time in milliseconds that triggers a warning only", + "format": "int32" } + }, + "required": [ + "type", + "warnMs" ] }, "ResultSummaryDto": { @@ -29507,91 +29343,81 @@ }, "SlackChannelConfig": { "required": [ + "channelType", "webhookUrl" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/ChannelConfig" - }, - { - "type": "object", - "properties": { - "webhookUrl": { - "minLength": 1, - "type": "string", - "description": "Slack incoming webhook URL" - }, - "mentionText": { - "type": "string", - "description": "Optional mention text included in notifications, e.g. @channel", - "nullable": true - } - }, - "required": [ - "webhookUrl" + "properties": { + "channelType": { + "type": "string", + "enum": [ + "slack" ] + }, + "webhookUrl": { + "minLength": 1, + "type": "string", + "description": "Slack incoming webhook URL" + }, + "mentionText": { + "type": "string", + "description": "Optional mention text included in notifications, e.g. @channel", + "nullable": true } - ] + } }, "SslExpiryAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "minDaysRemaining": { - "type": "integer", - "description": "Minimum days before TLS certificate expiry; fails or warns below this threshold", - "format": "int32" - } - }, - "required": [ - "minDaysRemaining" + "properties": { + "type": { + "type": "string", + "enum": [ + "ssl_expiry" ] + }, + "minDaysRemaining": { + "type": "integer", + "description": "Minimum days before TLS certificate expiry; fails or warns below this threshold", + "format": "int32" } + }, + "required": [ + "type", + "minDaysRemaining" ] }, "StatusCodeAssertion": { "required": [ + "type", "expected", "operator" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" + "properties": { + "type": { + "type": "string", + "enum": [ + "status_code" + ] }, - { - "type": "object", - "properties": { - "expected": { - "minLength": 1, - "type": "string", - "description": "Expected status code, range pattern, or wildcard such as 2xx" - }, - "operator": { - "type": "string", - "description": "Comparison operator (equals, contains, less_than, greater_than, etc.)", - "enum": [ - "equals", - "contains", - "less_than", - "greater_than", - "matches", - "range" - ] - } - }, - "required": [ - "expected", - "operator" + "expected": { + "minLength": 1, + "type": "string", + "description": "Expected status code, range pattern, or wildcard such as 2xx" + }, + "operator": { + "type": "string", + "description": "Comparison operator (equals, contains, less_than, greater_than, etc.)", + "enum": [ + "equals", + "contains", + "less_than", + "greater_than", + "matches", + "range" ] } - ] + } }, "StatusPageBranding": { "type": "object", @@ -31453,47 +31279,49 @@ }, "Tcp": { "required": [ - "host" + "check_type", + "host", + "port", + "connected" ], "type": "object", "description": "TCP check-type-specific details", - "allOf": [ - { - "$ref": "#/components/schemas/CheckTypeDetailsDto" - }, - { - "type": "object", - "properties": { - "host": { - "type": "string", - "description": "Target host", - "example": "db.example.com" - }, - "port": { - "type": "integer", - "description": "Target port", - "format": "int32", - "example": 5432 - }, - "connected": { - "type": "boolean", - "description": "Whether a TCP connection was established" - } - }, - "required": [ - "host", - "port", - "connected" + "properties": { + "check_type": { + "type": "string", + "enum": [ + "tcp" ] + }, + "host": { + "type": "string", + "description": "Target host", + "example": "db.example.com" + }, + "port": { + "type": "integer", + "description": "Target port", + "format": "int32", + "example": 5432 + }, + "connected": { + "type": "boolean", + "description": "Whether a TCP connection was established" } - ] + } }, "TcpConnectsAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" + "properties": { + "type": { + "type": "string", + "enum": [ + "tcp_connects" + ] } + }, + "required": [ + "type" ] }, "TcpMonitorConfig": { @@ -31536,69 +31364,63 @@ }, "TcpResponseTimeAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxMs": { - "type": "integer", - "description": "Maximum TCP connect time in milliseconds before the check fails", - "format": "int32" - } - }, - "required": [ - "maxMs" + "properties": { + "type": { + "type": "string", + "enum": [ + "tcp_response_time" ] + }, + "maxMs": { + "type": "integer", + "description": "Maximum TCP connect time in milliseconds before the check fails", + "format": "int32" } + }, + "required": [ + "type", + "maxMs" ] }, "TcpResponseTimeWarnAssertion": { "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "warnMs": { - "type": "integer", - "description": "TCP connect time in milliseconds that triggers a warning only", - "format": "int32" - } - }, - "required": [ - "warnMs" + "properties": { + "type": { + "type": "string", + "enum": [ + "tcp_response_time_warn" ] + }, + "warnMs": { + "type": "integer", + "description": "TCP connect time in milliseconds that triggers a warning only", + "format": "int32" } + }, + "required": [ + "type", + "warnMs" ] }, "TeamsChannelConfig": { "required": [ + "channelType", "webhookUrl" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/ChannelConfig" - }, - { - "type": "object", - "properties": { - "webhookUrl": { - "minLength": 1, - "type": "string", - "description": "Microsoft Teams incoming webhook URL" - } - }, - "required": [ - "webhookUrl" + "properties": { + "channelType": { + "type": "string", + "enum": [ + "teams" ] + }, + "webhookUrl": { + "minLength": 1, + "type": "string", + "description": "Microsoft Teams incoming webhook URL" } - ] + } }, "TestAlertChannelRequest": { "required": [ @@ -32852,42 +32674,38 @@ }, "WebhookChannelConfig": { "required": [ + "channelType", "url" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/ChannelConfig" + "properties": { + "channelType": { + "type": "string", + "enum": [ + "webhook" + ] }, - { + "url": { + "minLength": 1, + "type": "string", + "description": "Webhook endpoint URL that receives alert payloads" + }, + "signingSecret": { + "type": "string", + "description": "Optional HMAC signing secret for payload verification", + "nullable": true + }, + "customHeaders": { "type": "object", - "properties": { - "url": { - "minLength": 1, - "type": "string", - "description": "Webhook endpoint URL that receives alert payloads" - }, - "signingSecret": { - "type": "string", - "description": "Optional HMAC signing secret for payload verification", - "nullable": true - }, - "customHeaders": { - "type": "object", - "additionalProperties": { - "type": "string", - "description": "Additional HTTP headers to include in webhook requests", - "nullable": true - }, - "description": "Additional HTTP headers to include in webhook requests", - "nullable": true - } + "additionalProperties": { + "type": "string", + "description": "Additional HTTP headers to include in webhook requests", + "nullable": true }, - "required": [ - "url" - ] + "description": "Additional HTTP headers to include in webhook requests", + "nullable": true } - ] + } }, "WebhookDeliveryDto": { "required": [ diff --git a/src/devhelm/_generated.py b/src/devhelm/_generated.py index cbb8dc1..a2a3d03 100644 --- a/src/devhelm/_generated.py +++ b/src/devhelm/_generated.py @@ -1,9 +1,9 @@ # generated by datamodel-codegen: # filename: .openapi-preprocessed.json -# timestamp: 2026-04-20T12:58:15+00:00 +# timestamp: 2026-04-20T14:50:16+00:00 from __future__ import annotations -from typing import Annotated, Any +from typing import Annotated, Any, Literal from pydantic import AwareDatetime, BaseModel, EmailStr, Field, RootModel from enum import StrEnum from uuid import UUID @@ -286,6 +286,29 @@ class AlertDeliveryDto(BaseModel): created_at: Annotated[AwareDatetime, Field(alias="createdAt")] +class Type(StrEnum): + api_key = "api_key" + + +class ApiKeyAuthConfig(BaseModel): + type: Literal["api_key"] + header_name: Annotated[ + str, + Field( + alias="headerName", + description="HTTP header name that carries the API key", + min_length=1, + pattern="^[A-Za-z0-9\\-_]+$", + ), + ] + vault_secret_id: Annotated[ + UUID | None, + Field( + alias="vaultSecretId", description="Vault secret ID for the API key value" + ), + ] = None + + class ApiKeyCreateResponse(BaseModel): id: Annotated[int, Field(description="Unique API key identifier")] name: Annotated[ @@ -350,10 +373,6 @@ class ApiKeyDto(BaseModel): ] = None -class AssertionConfig(BaseModel): - type: str - - class Severity(StrEnum): fail = "fail" warn = "warn" @@ -480,7 +499,42 @@ class AuditEventDto(BaseModel): ] -class BodyContainsAssertion(AssertionConfig): +class Type1(StrEnum): + basic = "basic" + + +class BasicAuthConfig(BaseModel): + type: Literal["basic"] + vault_secret_id: Annotated[ + UUID | None, + Field( + alias="vaultSecretId", + description="Vault secret ID holding Basic auth username and password", + ), + ] = None + + +class Type2(StrEnum): + bearer = "bearer" + + +class BearerAuthConfig(BaseModel): + type: Literal["bearer"] + vault_secret_id: Annotated[ + UUID | None, + Field( + alias="vaultSecretId", + description="Vault secret ID holding the bearer token value", + ), + ] = None + + +class Type3(StrEnum): + body_contains = "body_contains" + + +class BodyContainsAssertion(BaseModel): + type: Type3 substring: Annotated[ str, Field( @@ -534,10 +588,6 @@ class ChangeStatusRequest(BaseModel): ] -class ChannelConfig(BaseModel): - channel_type: Annotated[str, Field(alias="channelType")] - - class ChartBucketDto(BaseModel): bucket: Annotated[ AwareDatetime, @@ -580,10 +630,6 @@ class ChartBucketDto(BaseModel): ] = None -class CheckTypeDetailsDto(BaseModel): - check_type: str - - class ComponentImpact(BaseModel): component_id: Annotated[ UUID, Field(alias="componentId", description="Status page component UUID") @@ -711,13 +757,13 @@ class ComponentUptimeSummaryDto(BaseModel): ] -class Type(StrEnum): +class Type4(StrEnum): multi_region = "multi_region" class ConfirmationPolicy(BaseModel): type: Annotated[ - Type, + Type4, Field(description="How incident confirmation is coordinated across regions"), ] min_regions_failing: Annotated[ @@ -866,7 +912,7 @@ class CreateManualIncidentRequest(BaseModel): ] = None -class Type1(StrEnum): +class Type5(StrEnum): http = "HTTP" dns = "DNS" mcp_server = "MCP_SERVER" @@ -923,7 +969,7 @@ class CreateStatusPageComponentGroupRequest(BaseModel): ] = None -class Type2(StrEnum): +class Type6(StrEnum): monitor = "MONITOR" group = "GROUP" static = "STATIC" @@ -942,7 +988,7 @@ class CreateStatusPageComponentRequest(BaseModel): ), ] = None type: Annotated[ - Type2, Field(description="Component type: MONITOR, GROUP, or STATIC") + Type6, Field(description="Component type: MONITOR, GROUP, or STATIC") ] monitor_id: Annotated[ UUID | None, @@ -1307,7 +1353,12 @@ class DeployLockDto(BaseModel): ] -class DiscordChannelConfig(ChannelConfig): +class ChannelType1(StrEnum): + discord = "discord" + + +class DiscordChannelConfig(BaseModel): + channel_type: Annotated[ChannelType1, Field(alias="channelType")] webhook_url: Annotated[ str, Field(alias="webhookUrl", description="Discord webhook URL", min_length=1) ] @@ -1320,7 +1371,12 @@ class DiscordChannelConfig(ChannelConfig): ] = None -class Dns(CheckTypeDetailsDto): +class CheckType(StrEnum): + dns = "dns" + + +class Dns(BaseModel): + check_type: Literal["dns"] hostname: Annotated[str | None, Field(description="Target hostname")] = None requested_types: Annotated[ list[str] | None, @@ -1343,7 +1399,12 @@ class Dns(CheckTypeDetailsDto): ] = None -class DnsExpectedCnameAssertion(AssertionConfig): +class Type7(StrEnum): + dns_expected_cname = "dns_expected_cname" + + +class DnsExpectedCnameAssertion(BaseModel): + type: Type7 value: Annotated[ str, Field( @@ -1353,7 +1414,12 @@ class DnsExpectedCnameAssertion(AssertionConfig): ] -class DnsExpectedIpsAssertion(AssertionConfig): +class Type8(StrEnum): + dns_expected_ips = "dns_expected_ips" + + +class DnsExpectedIpsAssertion(BaseModel): + type: Type8 ips: Annotated[ list[str], Field( @@ -1363,7 +1429,12 @@ class DnsExpectedIpsAssertion(AssertionConfig): ] -class DnsMaxAnswersAssertion(AssertionConfig): +class Type9(StrEnum): + dns_max_answers = "dns_max_answers" + + +class DnsMaxAnswersAssertion(BaseModel): + type: Type9 record_type: Annotated[ str, Field( @@ -1377,7 +1448,12 @@ class DnsMaxAnswersAssertion(AssertionConfig): ] -class DnsMinAnswersAssertion(AssertionConfig): +class Type10(StrEnum): + dns_min_answers = "dns_min_answers" + + +class DnsMinAnswersAssertion(BaseModel): + type: Type10 record_type: Annotated[ str, Field( @@ -1405,7 +1481,12 @@ class RecordType(StrEnum): ptr = "PTR" -class DnsRecordContainsAssertion(AssertionConfig): +class Type11(StrEnum): + dns_record_contains = "dns_record_contains" + + +class DnsRecordContainsAssertion(BaseModel): + type: Type11 record_type: Annotated[ str, Field( @@ -1423,7 +1504,12 @@ class DnsRecordContainsAssertion(AssertionConfig): ] -class DnsRecordEqualsAssertion(AssertionConfig): +class Type12(StrEnum): + dns_record_equals = "dns_record_equals" + + +class DnsRecordEqualsAssertion(BaseModel): + type: Type12 record_type: Annotated[ str, Field( @@ -1438,11 +1524,20 @@ class DnsRecordEqualsAssertion(AssertionConfig): ] -class DnsResolvesAssertion(AssertionConfig): - pass +class Type13(StrEnum): + dns_resolves = "dns_resolves" + + +class DnsResolvesAssertion(BaseModel): + type: Type13 + + +class Type14(StrEnum): + dns_response_time = "dns_response_time" -class DnsResponseTimeAssertion(AssertionConfig): +class DnsResponseTimeAssertion(BaseModel): + type: Type14 max_ms: Annotated[ int, Field( @@ -1452,7 +1547,12 @@ class DnsResponseTimeAssertion(AssertionConfig): ] -class DnsResponseTimeWarnAssertion(AssertionConfig): +class Type15(StrEnum): + dns_response_time_warn = "dns_response_time_warn" + + +class DnsResponseTimeWarnAssertion(BaseModel): + type: Type15 warn_ms: Annotated[ int, Field( @@ -1462,7 +1562,12 @@ class DnsResponseTimeWarnAssertion(AssertionConfig): ] -class DnsTtlHighAssertion(AssertionConfig): +class Type16(StrEnum): + dns_ttl_high = "dns_ttl_high" + + +class DnsTtlHighAssertion(BaseModel): + type: Type16 max_ttl: Annotated[ int, Field( @@ -1472,7 +1577,12 @@ class DnsTtlHighAssertion(AssertionConfig): ] -class DnsTtlLowAssertion(AssertionConfig): +class Type17(StrEnum): + dns_ttl_low = "dns_ttl_low" + + +class DnsTtlLowAssertion(BaseModel): + type: Type17 min_ttl: Annotated[ int, Field( @@ -1482,7 +1592,12 @@ class DnsTtlLowAssertion(AssertionConfig): ] -class DnsTxtContainsAssertion(AssertionConfig): +class Type18(StrEnum): + dns_txt_contains = "dns_txt_contains" + + +class DnsTxtContainsAssertion(BaseModel): + type: Type18 substring: Annotated[ str, Field( @@ -1492,7 +1607,12 @@ class DnsTxtContainsAssertion(AssertionConfig): ] -class EmailChannelConfig(ChannelConfig): +class ChannelType2(StrEnum): + email = "email" + + +class EmailChannelConfig(BaseModel): + channel_type: Annotated[ChannelType2, Field(alias="channelType")] recipients: Annotated[ list[EmailStr], Field(description="Email addresses to send notifications to", min_length=1), @@ -1636,6 +1756,33 @@ class GroupComponentOrder(BaseModel): ] +class Type19(StrEnum): + header = "header" + + +class HeaderAuthConfig(BaseModel): + type: Literal["header"] + header_name: Annotated[ + str, + Field( + alias="headerName", + description="Custom HTTP header name for the secret value", + min_length=1, + pattern="^[A-Za-z0-9\\-_]+$", + ), + ] + vault_secret_id: Annotated[ + UUID | None, + Field( + alias="vaultSecretId", description="Vault secret ID for the header value" + ), + ] = None + + +class Type20(StrEnum): + header_value = "header_value" + + class Operator(StrEnum): equals = "equals" contains = "contains" @@ -1645,7 +1792,8 @@ class Operator(StrEnum): range = "range" -class HeaderValueAssertion(AssertionConfig): +class HeaderValueAssertion(BaseModel): + type: Type20 header_name: Annotated[ str, Field( @@ -1665,7 +1813,12 @@ class HeaderValueAssertion(AssertionConfig): ] -class HeartbeatIntervalDriftAssertion(AssertionConfig): +class Type21(StrEnum): + heartbeat_interval_drift = "heartbeat_interval_drift" + + +class HeartbeatIntervalDriftAssertion(BaseModel): + type: Type21 max_deviation_percent: Annotated[ int, Field( @@ -1677,7 +1830,12 @@ class HeartbeatIntervalDriftAssertion(AssertionConfig): ] -class HeartbeatMaxIntervalAssertion(AssertionConfig): +class Type22(StrEnum): + heartbeat_max_interval = "heartbeat_max_interval" + + +class HeartbeatMaxIntervalAssertion(BaseModel): + type: Type22 max_seconds: Annotated[ int, Field( @@ -1688,7 +1846,12 @@ class HeartbeatMaxIntervalAssertion(AssertionConfig): ] -class HeartbeatPayloadContainsAssertion(AssertionConfig): +class Type23(StrEnum): + heartbeat_payload_contains = "heartbeat_payload_contains" + + +class HeartbeatPayloadContainsAssertion(BaseModel): + type: Type23 path: Annotated[ str, Field( @@ -1707,11 +1870,20 @@ class HeartbeatPingResponse(BaseModel): ] -class HeartbeatReceivedAssertion(AssertionConfig): - pass +class Type24(StrEnum): + heartbeat_received = "heartbeat_received" + +class HeartbeatReceivedAssertion(BaseModel): + type: Type24 -class Http(CheckTypeDetailsDto): + +class CheckType1(StrEnum): + http = "http" + + +class Http(BaseModel): + check_type: Literal["http"] timing: Annotated[ dict[str, dict[str, Any]] | None, Field(description="Request phase timing breakdown"), @@ -1734,7 +1906,12 @@ class Method(StrEnum): head = "HEAD" -class Icmp(CheckTypeDetailsDto): +class CheckType2(StrEnum): + icmp = "icmp" + + +class Icmp(BaseModel): + check_type: Literal["icmp"] host: Annotated[str, Field(description="Target host", examples=["1.1.1.1"])] packets_sent: Annotated[ int | None, @@ -1765,7 +1942,12 @@ class Icmp(CheckTypeDetailsDto): ] = None -class IcmpPacketLossAssertion(AssertionConfig): +class Type25(StrEnum): + icmp_packet_loss = "icmp_packet_loss" + + +class IcmpPacketLossAssertion(BaseModel): + type: Type25 max_percent: Annotated[ float, Field( @@ -1777,11 +1959,20 @@ class IcmpPacketLossAssertion(AssertionConfig): ] -class IcmpReachableAssertion(AssertionConfig): - pass +class Type26(StrEnum): + icmp_reachable = "icmp_reachable" + + +class IcmpReachableAssertion(BaseModel): + type: Type26 -class IcmpResponseTimeAssertion(AssertionConfig): +class Type27(StrEnum): + icmp_response_time = "icmp_response_time" + + +class IcmpResponseTimeAssertion(BaseModel): + type: Type27 max_ms: Annotated[ int, Field( @@ -1791,7 +1982,12 @@ class IcmpResponseTimeAssertion(AssertionConfig): ] -class IcmpResponseTimeWarnAssertion(AssertionConfig): +class Type28(StrEnum): + icmp_response_time_warn = "icmp_response_time_warn" + + +class IcmpResponseTimeWarnAssertion(BaseModel): + type: Type28 warn_ms: Annotated[ int, Field( @@ -2178,7 +2374,12 @@ class InviteDto(BaseModel): ] = None -class JsonPathAssertion(AssertionConfig): +class Type29(StrEnum): + json_path = "json_path" + + +class JsonPathAssertion(BaseModel): + type: Type29 path: Annotated[ str, Field( @@ -2325,11 +2526,20 @@ class MatchRule(BaseModel): ] = None -class McpConnectsAssertion(AssertionConfig): - pass +class Type30(StrEnum): + mcp_connects = "mcp_connects" + +class McpConnectsAssertion(BaseModel): + type: Type30 -class McpHasCapabilityAssertion(AssertionConfig): + +class Type31(StrEnum): + mcp_has_capability = "mcp_has_capability" + + +class McpHasCapabilityAssertion(BaseModel): + type: Type31 capability: Annotated[ str, Field( @@ -2339,13 +2549,23 @@ class McpHasCapabilityAssertion(AssertionConfig): ] -class McpMinToolsAssertion(AssertionConfig): +class Type32(StrEnum): + mcp_min_tools = "mcp_min_tools" + + +class McpMinToolsAssertion(BaseModel): + type: Type32 min: Annotated[ int, Field(description="Minimum number of tools the server must expose") ] -class McpProtocolVersionAssertion(AssertionConfig): +class Type33(StrEnum): + mcp_protocol_version = "mcp_protocol_version" + + +class McpProtocolVersionAssertion(BaseModel): + type: Type33 version: Annotated[ str, Field( @@ -2355,7 +2575,12 @@ class McpProtocolVersionAssertion(AssertionConfig): ] -class McpResponseTimeAssertion(AssertionConfig): +class Type34(StrEnum): + mcp_response_time = "mcp_response_time" + + +class McpResponseTimeAssertion(BaseModel): + type: Type34 max_ms: Annotated[ int, Field( @@ -2365,7 +2590,12 @@ class McpResponseTimeAssertion(AssertionConfig): ] -class McpResponseTimeWarnAssertion(AssertionConfig): +class Type35(StrEnum): + mcp_response_time_warn = "mcp_response_time_warn" + + +class McpResponseTimeWarnAssertion(BaseModel): + type: Type35 warn_ms: Annotated[ int, Field( @@ -2375,7 +2605,12 @@ class McpResponseTimeWarnAssertion(AssertionConfig): ] -class McpServer(CheckTypeDetailsDto): +class CheckType3(StrEnum): + mcp_server = "mcp_server" + + +class McpServer(BaseModel): + check_type: Literal["mcp_server"] url: Annotated[str | None, Field(description="MCP server URL")] = None protocol_version: Annotated[ str | None, Field(alias="protocolVersion", description="MCP protocol version") @@ -2396,7 +2631,12 @@ class McpServer(CheckTypeDetailsDto): ] = None -class McpToolAvailableAssertion(AssertionConfig): +class Type36(StrEnum): + mcp_tool_available = "mcp_tool_available" + + +class McpToolAvailableAssertion(BaseModel): + type: Type36 tool_name: Annotated[ str, Field( @@ -2407,7 +2647,12 @@ class McpToolAvailableAssertion(AssertionConfig): ] -class McpToolCountChangedAssertion(AssertionConfig): +class Type37(StrEnum): + mcp_tool_count_changed = "mcp_tool_count_changed" + + +class McpToolCountChangedAssertion(BaseModel): + type: Type37 expected_count: Annotated[ int, Field( @@ -2458,8 +2703,16 @@ class Severity6(StrEnum): warn = "warn" -class MonitorAuthConfig(BaseModel): - type: str +class MonitorAuthConfig( + RootModel[BearerAuthConfig | BasicAuthConfig | HeaderAuthConfig | ApiKeyAuthConfig] +): + root: Annotated[ + BearerAuthConfig | BasicAuthConfig | HeaderAuthConfig | ApiKeyAuthConfig, + Field( + description="New authentication configuration (full replacement)", + discriminator="type", + ), + ] class AuthType(StrEnum): @@ -2469,11 +2722,18 @@ class AuthType(StrEnum): api_key = "api_key" +class MonitorAuthDto(BaseModel): + id: UUID + monitor_id: Annotated[UUID, Field(alias="monitorId")] + auth_type: Annotated[AuthType, Field(alias="authType")] + config: ApiKeyAuthConfig | BasicAuthConfig | BearerAuthConfig | HeaderAuthConfig + + class MonitorConfig(BaseModel): pass -class Type3(StrEnum): +class Type38(StrEnum): http = "HTTP" dns = "DNS" mcp_server = "MCP_SERVER" @@ -2680,7 +2940,12 @@ class NotificationDto(BaseModel): ] -class OpsGenieChannelConfig(ChannelConfig): +class ChannelType3(StrEnum): + opsgenie = "opsgenie" + + +class OpsGenieChannelConfig(BaseModel): + channel_type: Annotated[ChannelType3, Field(alias="channelType")] api_key: Annotated[ str, Field( @@ -2720,7 +2985,12 @@ class Pageable(BaseModel): sort: list[str] -class PagerDutyChannelConfig(ChannelConfig): +class ChannelType4(StrEnum): + pagerduty = "pagerduty" + + +class PagerDutyChannelConfig(BaseModel): + channel_type: Annotated[ChannelType4, Field(alias="channelType")] routing_key: Annotated[ str, Field( @@ -2902,7 +3172,12 @@ class RecoveryPolicy(BaseModel): ] -class RedirectCountAssertion(AssertionConfig): +class Type40(StrEnum): + redirect_count = "redirect_count" + + +class RedirectCountAssertion(BaseModel): + type: Type40 max_count: Annotated[ int, Field( @@ -2912,7 +3187,12 @@ class RedirectCountAssertion(AssertionConfig): ] -class RedirectTargetAssertion(AssertionConfig): +class Type41(StrEnum): + redirect_target = "redirect_target" + + +class RedirectTargetAssertion(BaseModel): + type: Type41 expected: Annotated[ str, Field(description="Expected final URL after following redirects", min_length=1), @@ -2925,7 +3205,12 @@ class RedirectTargetAssertion(AssertionConfig): ] -class RegexBodyAssertion(AssertionConfig): +class Type42(StrEnum): + regex_body = "regex_body" + + +class RegexBodyAssertion(BaseModel): + type: Type42 pattern: Annotated[ str, Field( @@ -3159,7 +3444,12 @@ class ResourceGroupMemberDto(BaseModel): ] = None -class ResponseSizeAssertion(AssertionConfig): +class Type43(StrEnum): + response_size = "response_size" + + +class ResponseSizeAssertion(BaseModel): + type: Type43 max_bytes: Annotated[ int, Field( @@ -3169,7 +3459,12 @@ class ResponseSizeAssertion(AssertionConfig): ] -class ResponseTimeAssertion(AssertionConfig): +class Type44(StrEnum): + response_time = "response_time" + + +class ResponseTimeAssertion(BaseModel): + type: Type44 threshold_ms: Annotated[ int, Field( @@ -3179,7 +3474,12 @@ class ResponseTimeAssertion(AssertionConfig): ] -class ResponseTimeWarnAssertion(AssertionConfig): +class Type45(StrEnum): + response_time_warn = "response_time_warn" + + +class ResponseTimeWarnAssertion(BaseModel): + type: Type45 warn_ms: Annotated[ int, Field( @@ -3751,6 +4051,10 @@ class SetAlertChannelsRequest(BaseModel): ] +class SetMonitorAuthRequest(BaseModel): + config: ApiKeyAuthConfig | BasicAuthConfig | BearerAuthConfig | HeaderAuthConfig + + class SingleValueResponseAlertChannelDto(BaseModel): data: AlertChannelDto @@ -3795,6 +4099,10 @@ class SingleValueResponseMaintenanceWindowDto(BaseModel): data: MaintenanceWindowDto +class SingleValueResponseMonitorAuthDto(BaseModel): + data: MonitorAuthDto + + class SingleValueResponseMonitorTestResultDto(BaseModel): data: MonitorTestResultDto @@ -3843,7 +4151,12 @@ class SingleValueResponseString(BaseModel): data: str -class SlackChannelConfig(ChannelConfig): +class ChannelType5(StrEnum): + slack = "slack" + + +class SlackChannelConfig(BaseModel): + channel_type: Annotated[ChannelType5, Field(alias="channelType")] webhook_url: Annotated[ str, Field( @@ -3859,7 +4172,12 @@ class SlackChannelConfig(ChannelConfig): ] = None -class SslExpiryAssertion(AssertionConfig): +class Type46(StrEnum): + ssl_expiry = "ssl_expiry" + + +class SslExpiryAssertion(BaseModel): + type: Type46 min_days_remaining: Annotated[ int, Field( @@ -3869,7 +4187,12 @@ class SslExpiryAssertion(AssertionConfig): ] -class StatusCodeAssertion(AssertionConfig): +class Type47(StrEnum): + status_code = "status_code" + + +class StatusCodeAssertion(BaseModel): + type: Type47 expected: Annotated[ str, Field( @@ -4010,7 +4333,7 @@ class StatusPageBranding(BaseModel): ] = None -class Type5(StrEnum): +class Type48(StrEnum): monitor = "MONITOR" group = "GROUP" static = "STATIC" @@ -4030,7 +4353,7 @@ class StatusPageComponentDto(BaseModel): group_id: Annotated[UUID | None, Field(alias="groupId")] = None name: Annotated[str, Field(min_length=1)] description: str | None = None - type: Type5 + type: Type48 monitor_id: Annotated[UUID | None, Field(alias="monitorId")] = None resource_group_id: Annotated[UUID | None, Field(alias="resourceGroupId")] = None current_status: Annotated[CurrentStatus1, Field(alias="currentStatus")] @@ -4417,7 +4740,12 @@ class TagDto(BaseModel): ] -class Tcp(CheckTypeDetailsDto): +class CheckType4(StrEnum): + tcp = "tcp" + + +class Tcp(BaseModel): + check_type: Literal["tcp"] host: Annotated[str, Field(description="Target host", examples=["db.example.com"])] port: Annotated[int, Field(description="Target port", examples=[5432])] connected: Annotated[ @@ -4425,8 +4753,12 @@ class Tcp(CheckTypeDetailsDto): ] -class TcpConnectsAssertion(AssertionConfig): - pass +class Type49(StrEnum): + tcp_connects = "tcp_connects" + + +class TcpConnectsAssertion(BaseModel): + type: Type49 class TcpMonitorConfig(MonitorConfig): @@ -4440,7 +4772,12 @@ class TcpMonitorConfig(MonitorConfig): ] = None -class TcpResponseTimeAssertion(AssertionConfig): +class Type50(StrEnum): + tcp_response_time = "tcp_response_time" + + +class TcpResponseTimeAssertion(BaseModel): + type: Type50 max_ms: Annotated[ int, Field( @@ -4450,7 +4787,12 @@ class TcpResponseTimeAssertion(AssertionConfig): ] -class TcpResponseTimeWarnAssertion(AssertionConfig): +class Type51(StrEnum): + tcp_response_time_warn = "tcp_response_time_warn" + + +class TcpResponseTimeWarnAssertion(BaseModel): + type: Type51 warn_ms: Annotated[ int, Field( @@ -4460,7 +4802,12 @@ class TcpResponseTimeWarnAssertion(AssertionConfig): ] -class TeamsChannelConfig(ChannelConfig): +class ChannelType6(StrEnum): + teams = "teams" + + +class TeamsChannelConfig(BaseModel): + channel_type: Annotated[ChannelType6, Field(alias="channelType")] webhook_url: Annotated[ str, Field( @@ -4616,7 +4963,7 @@ class TlsInfoDto(BaseModel): ] = None -class Type6(StrEnum): +class Type52(StrEnum): consecutive_failures = "consecutive_failures" failures_in_window = "failures_in_window" response_time = "response_time" @@ -4641,7 +4988,7 @@ class AggregationType(StrEnum): class TriggerRule(BaseModel): type: Annotated[ - Type6, + Type52, Field( description="Condition that opens or escalates an incident from check results" ), @@ -4825,6 +5172,10 @@ class UpdateMaintenanceWindowRequest(BaseModel): ] = None +class UpdateMonitorAuthRequest(BaseModel): + config: ApiKeyAuthConfig | BasicAuthConfig | BearerAuthConfig | HeaderAuthConfig + + class UpdateOrgDetailsRequest(BaseModel): name: Annotated[ str, @@ -5256,7 +5607,12 @@ class UptimeDto(BaseModel): ] = None -class WebhookChannelConfig(ChannelConfig): +class ChannelType7(StrEnum): + webhook = "webhook" + + +class WebhookChannelConfig(BaseModel): + channel_type: Annotated[ChannelType7, Field(alias="channelType")] url: Annotated[ str, Field( @@ -5418,24 +5774,6 @@ class AddMonitorTagsRequest(BaseModel): ] = None -class ApiKeyAuthConfig(MonitorAuthConfig): - header_name: Annotated[ - str, - Field( - alias="headerName", - description="HTTP header name that carries the API key", - min_length=1, - pattern="^[A-Za-z0-9\\-_]+$", - ), - ] - vault_secret_id: Annotated[ - UUID | None, - Field( - alias="vaultSecretId", description="Vault secret ID for the API key value" - ), - ] = None - - class AuthMeResponse(BaseModel): key: KeyInfo organization: OrgInfo @@ -5443,16 +5781,6 @@ class AuthMeResponse(BaseModel): rate_limits: Annotated[RateLimitInfo, Field(alias="rateLimits")] -class BasicAuthConfig(MonitorAuthConfig): - vault_secret_id: Annotated[ - UUID | None, - Field( - alias="vaultSecretId", - description="Vault secret ID holding Basic auth username and password", - ), - ] = None - - class BatchComponentUptimeDto(BaseModel): components: Annotated[ dict[str, list[ComponentUptimeDayDto]], @@ -5462,16 +5790,6 @@ class BatchComponentUptimeDto(BaseModel): ] -class BearerAuthConfig(MonitorAuthConfig): - vault_secret_id: Annotated[ - UUID | None, - Field( - alias="vaultSecretId", - description="Vault secret ID holding the bearer token value", - ), - ] = None - - class BulkMonitorActionRequest(BaseModel): monitor_ids: Annotated[ list[UUID], @@ -5514,101 +5832,14 @@ class BulkMonitorActionResult(BaseModel): ] -class CheckResultDetailsDto(BaseModel): - status_code: Annotated[ - int | None, - Field( - alias="statusCode", - description="HTTP status code of the response", - examples=[200], - ), - ] = None - response_headers: Annotated[ - dict[str, list[str]] | None, - Field(alias="responseHeaders", description="HTTP response headers"), - ] = None - response_body_snapshot: Annotated[ - str | None, - Field( - alias="responseBodySnapshot", - description="Raw response body snapshot (may be HTML, XML, JSON, or plain text)", - ), - ] = None - assertion_results: Annotated[ - list[AssertionResultDto] | None, - Field( - alias="assertionResults", - description="Individual assertion evaluation results", - ), - ] = None - tls_info: Annotated[TlsInfoDto | None, Field(alias="tlsInfo")] = None - redirect_count: Annotated[ - int | None, - Field( - alias="redirectCount", - description="Number of HTTP redirects followed", - examples=[2], - ), - ] = None - redirect_target: Annotated[ - str | None, - Field(alias="redirectTarget", description="Final URL after redirects"), - ] = None - response_size_bytes: Annotated[ - int | None, - Field( - alias="responseSizeBytes", - description="Response body size in bytes", - examples=[4096], - ), - ] = None - check_details: Annotated[ - Dns | Http | Icmp | McpServer | Tcp | None, Field(alias="checkDetails") - ] = None - - -class CheckResultDto(BaseModel): - id: Annotated[UUID, Field(description="Unique identifier of the check result")] - timestamp: Annotated[ - AwareDatetime, - Field(description="Timestamp when the check was executed (ISO 8601)"), - ] - region: Annotated[ - str, - Field(description="Region where the check was executed", examples=["us-east"]), - ] - response_time_ms: Annotated[ - int | None, +class CheckTypeDetailsDto(RootModel[Http | Tcp | Icmp | Dns | McpServer]): + root: Annotated[ + Http | Tcp | Icmp | Dns | McpServer, Field( - alias="responseTimeMs", - description="Response time in milliseconds", - examples=[123], + description="Check-type-specific details — polymorphic by check_type discriminator", + discriminator="check_type", ), - ] = None - passed: Annotated[ - bool, Field(description="Whether the check passed", examples=[True]) ] - failure_reason: Annotated[ - str | None, - Field( - alias="failureReason", description="Reason for failure when passed=false" - ), - ] = None - severity_hint: Annotated[ - str | None, - Field( - alias="severityHint", - description="Severity hint: 'down' for hard failures, 'degraded' for warn-only failures, null when passing", - ), - ] = None - details: CheckResultDetailsDto | None = None - check_id: Annotated[ - UUID | None, - Field( - alias="checkId", - description="Unique execution trace ID for cross-service correlation", - ), - ] = None class CreateAlertChannelRequest(BaseModel): @@ -5822,23 +6053,6 @@ class CreateStatusPageRequest(BaseModel): ] = None -class CursorPageCheckResultDto(BaseModel): - data: Annotated[list[CheckResultDto], Field(description="Items on this page")] - next_cursor: Annotated[ - str | None, - Field( - alias="nextCursor", - description="Opaque cursor for the next page; null when there are no more results", - ), - ] = None - has_more: Annotated[ - bool, - Field( - alias="hasMore", description="Whether more results exist beyond this page" - ), - ] - - class CursorPageServiceCatalogDto(BaseModel): data: Annotated[list[ServiceCatalogDto], Field(description="Items on this page")] next_cursor: Annotated[ @@ -5986,24 +6200,6 @@ class GlobalStatusSummaryDto(BaseModel): ] -class HeaderAuthConfig(MonitorAuthConfig): - header_name: Annotated[ - str, - Field( - alias="headerName", - description="Custom HTTP header name for the secret value", - min_length=1, - pattern="^[A-Za-z0-9\\-_]+$", - ), - ] - vault_secret_id: Annotated[ - UUID | None, - Field( - alias="vaultSecretId", description="Vault secret ID for the header value" - ), - ] = None - - class HeartbeatMonitorConfig(MonitorConfig): expected_interval: Annotated[ int, @@ -6212,13 +6408,6 @@ class MonitorAssertionDto(BaseModel): severity: Severity6 -class MonitorAuthDto(BaseModel): - id: UUID - monitor_id: Annotated[UUID, Field(alias="monitorId")] - auth_type: Annotated[AuthType, Field(alias="authType")] - config: ApiKeyAuthConfig | BasicAuthConfig | BearerAuthConfig | HeaderAuthConfig - - class MonitorDto(BaseModel): id: Annotated[UUID, Field(description="Unique monitor identifier")] organization_id: Annotated[ @@ -6230,7 +6419,7 @@ class MonitorDto(BaseModel): name: Annotated[ str, Field(description="Human-readable name for this monitor", min_length=1) ] - type: Type3 + type: Type38 config: ( DnsMonitorConfig | HeartbeatMonitorConfig @@ -6281,9 +6470,7 @@ class MonitorDto(BaseModel): ), ] = None environment: Summary | None = None - auth: ( - ApiKeyAuthConfig | BasicAuthConfig | BearerAuthConfig | HeaderAuthConfig | None - ) = None + auth: MonitorAuthConfig | None = None incident_policy: Annotated[ IncidentPolicyDto | None, Field(alias="incidentPolicy") ] = None @@ -6297,7 +6484,7 @@ class MonitorDto(BaseModel): class MonitorTestRequest(BaseModel): - type: Annotated[Type3, Field(description="Monitor protocol type to test")] + type: Annotated[Type38, Field(description="Monitor protocol type to test")] config: ( DnsMonitorConfig | HeartbeatMonitorConfig @@ -6553,10 +6740,6 @@ class ServiceUptimeResponse(BaseModel): ] = None -class SetMonitorAuthRequest(BaseModel): - config: ApiKeyAuthConfig | BasicAuthConfig | BearerAuthConfig | HeaderAuthConfig - - class SingleValueResponseAuthMeResponse(BaseModel): data: AuthMeResponse @@ -6589,10 +6772,6 @@ class SingleValueResponseMonitorAssertionDto(BaseModel): data: MonitorAssertionDto -class SingleValueResponseMonitorAuthDto(BaseModel): - data: MonitorAuthDto - - class SingleValueResponseMonitorDto(BaseModel): data: MonitorDto @@ -6812,10 +6991,6 @@ class UpdateAlertChannelRequest(BaseModel): ) -class UpdateMonitorAuthRequest(BaseModel): - config: ApiKeyAuthConfig | BasicAuthConfig | BearerAuthConfig | HeaderAuthConfig - - class UpdateMonitorRequest(BaseModel): name: Annotated[ str | None, @@ -6865,9 +7040,7 @@ class UpdateMonitorRequest(BaseModel): list[CreateAssertionRequest] | None, Field(description="Replace all assertions; null preserves current"), ] = None - auth: ( - ApiKeyAuthConfig | BasicAuthConfig | BearerAuthConfig | HeaderAuthConfig | None - ) = None + auth: MonitorAuthConfig | None = None clear_auth: Annotated[ bool | None, Field(alias="clearAuth", description="Set to true to remove authentication"), @@ -6914,6 +7087,103 @@ class UpdateNotificationPolicyRequest(BaseModel): ] = None +class CheckResultDetailsDto(BaseModel): + status_code: Annotated[ + int | None, + Field( + alias="statusCode", + description="HTTP status code of the response", + examples=[200], + ), + ] = None + response_headers: Annotated[ + dict[str, list[str]] | None, + Field(alias="responseHeaders", description="HTTP response headers"), + ] = None + response_body_snapshot: Annotated[ + str | None, + Field( + alias="responseBodySnapshot", + description="Raw response body snapshot (may be HTML, XML, JSON, or plain text)", + ), + ] = None + assertion_results: Annotated[ + list[AssertionResultDto] | None, + Field( + alias="assertionResults", + description="Individual assertion evaluation results", + ), + ] = None + tls_info: Annotated[TlsInfoDto | None, Field(alias="tlsInfo")] = None + redirect_count: Annotated[ + int | None, + Field( + alias="redirectCount", + description="Number of HTTP redirects followed", + examples=[2], + ), + ] = None + redirect_target: Annotated[ + str | None, + Field(alias="redirectTarget", description="Final URL after redirects"), + ] = None + response_size_bytes: Annotated[ + int | None, + Field( + alias="responseSizeBytes", + description="Response body size in bytes", + examples=[4096], + ), + ] = None + check_details: Annotated[ + CheckTypeDetailsDto | None, Field(alias="checkDetails") + ] = None + + +class CheckResultDto(BaseModel): + id: Annotated[UUID, Field(description="Unique identifier of the check result")] + timestamp: Annotated[ + AwareDatetime, + Field(description="Timestamp when the check was executed (ISO 8601)"), + ] + region: Annotated[ + str, + Field(description="Region where the check was executed", examples=["us-east"]), + ] + response_time_ms: Annotated[ + int | None, + Field( + alias="responseTimeMs", + description="Response time in milliseconds", + examples=[123], + ), + ] = None + passed: Annotated[ + bool, Field(description="Whether the check passed", examples=[True]) + ] + failure_reason: Annotated[ + str | None, + Field( + alias="failureReason", description="Reason for failure when passed=false" + ), + ] = None + severity_hint: Annotated[ + str | None, + Field( + alias="severityHint", + description="Severity hint: 'down' for hard failures, 'degraded' for warn-only failures, null when passing", + ), + ] = None + details: CheckResultDetailsDto | None = None + check_id: Annotated[ + UUID | None, + Field( + alias="checkId", + description="Unique execution trace ID for cross-service correlation", + ), + ] = None + + class CreateMonitorRequest(BaseModel): name: Annotated[ str, @@ -6923,7 +7193,7 @@ class CreateMonitorRequest(BaseModel): min_length=0, ), ] - type: Annotated[Type1, Field(description="Monitor protocol type")] + type: Annotated[Type5, Field(description="Monitor protocol type")] config: ( DnsMonitorConfig | HeartbeatMonitorConfig @@ -6963,9 +7233,7 @@ class CreateMonitorRequest(BaseModel): list[CreateAssertionRequest] | None, Field(description="Assertions to evaluate against each check result"), ] = None - auth: ( - ApiKeyAuthConfig | BasicAuthConfig | BearerAuthConfig | HeaderAuthConfig | None - ) = None + auth: MonitorAuthConfig | None = None incident_policy: Annotated[ UpdateIncidentPolicyRequest | None, Field(alias="incidentPolicy") ] = None @@ -7007,5 +7275,22 @@ class CreateNotificationPolicyRequest(BaseModel): ] = 0 +class CursorPageCheckResultDto(BaseModel): + data: Annotated[list[CheckResultDto], Field(description="Items on this page")] + next_cursor: Annotated[ + str | None, + Field( + alias="nextCursor", + description="Opaque cursor for the next page; null when there are no more results", + ), + ] = None + has_more: Annotated[ + bool, + Field( + alias="hasMore", description="Whether more results exist beyond this page" + ), + ] + + class SingleValueResponseStatusPageIncidentDto(BaseModel): data: StatusPageIncidentDto From 454ae81bca326c6ddd706cac3cceec53ad0f72c4 Mon Sep 17 00:00:00 2001 From: caballeto Date: Mon, 20 Apr 2026 17:35:18 +0200 Subject: [PATCH 09/10] chore(types): export PublishStatusPageIncidentRequest Surface the typed request model so downstream surfaces (MCP server, tests) can import it from `devhelm.types` directly instead of reaching into `devhelm._generated`. Made-with: Cursor --- src/devhelm/types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/devhelm/types.py b/src/devhelm/types.py index 7a6d6b9..7852f44 100644 --- a/src/devhelm/types.py +++ b/src/devhelm/types.py @@ -78,6 +78,7 @@ NotificationPolicyDto, Operator, OrgRole, + PublishStatusPageIncidentRequest, RecordType, ReorderComponentsRequest, ResolutionReason, @@ -247,6 +248,7 @@ "MonitorDto", "MonitorVersionDto", "NotificationPolicyDto", + "PublishStatusPageIncidentRequest", "ReorderComponentsRequest", "ResolveIncidentRequest", "ResourceGroupDto", From eb8e5163ca4fb75138bc58967f15207250144c58 Mon Sep 17 00:00:00 2001 From: caballeto Date: Mon, 20 Apr 2026 19:01:03 +0200 Subject: [PATCH 10/10] =?UTF-8?q?chore:=20refresh=20spec=20=E2=80=94=20pro?= =?UTF-8?q?per=20MonitorConfig=20polymorphism=20in=20Pydantic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regenerate the Pydantic models against the updated OpenAPI spec. The mono-side preprocessor now inlines nullable references to deduction- based polymorphic parents, so `UpdateMonitorRequest.config` resolves to a proper `DnsMonitorConfig | HttpMonitorConfig | ... | None` union instead of a vacuous reference to the empty `MonitorConfig` base. No source changes; generated code only. Made-with: Cursor --- docs/openapi/monitoring-api.json | 419 +++++++++++++------------------ src/devhelm/_generated.py | 380 ++++++++++++++-------------- 2 files changed, 373 insertions(+), 426 deletions(-) diff --git a/docs/openapi/monitoring-api.json b/docs/openapi/monitoring-api.json index 83a003b..97856bf 100644 --- a/docs/openapi/monitoring-api.json +++ b/docs/openapi/monitoring-api.json @@ -23543,68 +23543,57 @@ "hostname" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorConfig" + "properties": { + "hostname": { + "minLength": 1, + "type": "string", + "description": "Domain name to resolve" }, - { - "type": "object", - "properties": { - "hostname": { - "minLength": 1, - "type": "string", - "description": "Domain name to resolve" - }, - "recordTypes": { - "type": "array", - "description": "DNS record types to query: A, AAAA, CNAME, MX, NS, TXT, SRV, SOA, CAA, PTR", - "nullable": true, - "items": { - "type": "string", - "description": "DNS record types to query: A, AAAA, CNAME, MX, NS, TXT, SRV, SOA, CAA, PTR", - "nullable": true, - "enum": [ - "A", - "AAAA", - "CNAME", - "MX", - "NS", - "TXT", - "SRV", - "SOA", - "CAA", - "PTR" - ] - } - }, - "nameservers": { - "type": "array", - "description": "Custom nameservers to query (uses system defaults if omitted)", - "nullable": true, - "items": { - "type": "string", - "description": "Custom nameservers to query (uses system defaults if omitted)", - "nullable": true - } - }, - "timeoutMs": { - "type": "integer", - "description": "Per-query timeout in milliseconds", - "format": "int32", - "nullable": true - }, - "totalTimeoutMs": { - "type": "integer", - "description": "Total timeout for all queries in milliseconds", - "format": "int32", - "nullable": true - } - }, - "required": [ - "hostname" - ] + "recordTypes": { + "type": "array", + "description": "DNS record types to query: A, AAAA, CNAME, MX, NS, TXT, SRV, SOA, CAA, PTR", + "nullable": true, + "items": { + "type": "string", + "description": "DNS record types to query: A, AAAA, CNAME, MX, NS, TXT, SRV, SOA, CAA, PTR", + "nullable": true, + "enum": [ + "A", + "AAAA", + "CNAME", + "MX", + "NS", + "TXT", + "SRV", + "SOA", + "CAA", + "PTR" + ] + } + }, + "nameservers": { + "type": "array", + "description": "Custom nameservers to query (uses system defaults if omitted)", + "nullable": true, + "items": { + "type": "string", + "description": "Custom nameservers to query (uses system defaults if omitted)", + "nullable": true + } + }, + "timeoutMs": { + "type": "integer", + "description": "Per-query timeout in milliseconds", + "format": "int32", + "nullable": true + }, + "totalTimeoutMs": { + "type": "integer", + "description": "Total timeout for all queries in milliseconds", + "format": "int32", + "nullable": true } - ] + } }, "DnsRecordContainsAssertion": { "required": [ @@ -24210,33 +24199,21 @@ "gracePeriod" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorConfig" + "properties": { + "expectedInterval": { + "maximum": 86400, + "minimum": 1, + "type": "integer", + "description": "Expected heartbeat interval in seconds", + "format": "int32" }, - { - "type": "object", - "properties": { - "expectedInterval": { - "maximum": 86400, - "minimum": 1, - "type": "integer", - "description": "Expected heartbeat interval in seconds", - "format": "int32" - }, - "gracePeriod": { - "minimum": 1, - "type": "integer", - "description": "Grace period in seconds before marking as down", - "format": "int32" - } - }, - "required": [ - "expectedInterval", - "gracePeriod" - ] + "gracePeriod": { + "minimum": 1, + "type": "integer", + "description": "Grace period in seconds before marking as down", + "format": "int32" } - ] + } }, "HeartbeatPayloadContainsAssertion": { "required": [ @@ -24327,62 +24304,50 @@ "url" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorConfig" + "properties": { + "url": { + "minLength": 1, + "type": "string", + "description": "Target URL to send requests to" }, - { + "method": { + "type": "string", + "description": "HTTP method: GET, POST, PUT, PATCH, DELETE, or HEAD", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "HEAD" + ] + }, + "customHeaders": { "type": "object", - "properties": { - "url": { - "minLength": 1, - "type": "string", - "description": "Target URL to send requests to" - }, - "method": { - "type": "string", - "description": "HTTP method: GET, POST, PUT, PATCH, DELETE, or HEAD", - "enum": [ - "GET", - "POST", - "PUT", - "PATCH", - "DELETE", - "HEAD" - ] - }, - "customHeaders": { - "type": "object", - "additionalProperties": { - "type": "string", - "description": "Additional HTTP headers to include in requests", - "nullable": true - }, - "description": "Additional HTTP headers to include in requests", - "nullable": true - }, - "requestBody": { - "type": "string", - "description": "Request body content for POST/PUT/PATCH methods", - "nullable": true - }, - "contentType": { - "type": "string", - "description": "Content-Type header value for the request body", - "nullable": true - }, - "verifyTls": { - "type": "boolean", - "description": "Whether to verify TLS certificates (default: true)", - "nullable": true - } + "additionalProperties": { + "type": "string", + "description": "Additional HTTP headers to include in requests", + "nullable": true }, - "required": [ - "url", - "method" - ] + "description": "Additional HTTP headers to include in requests", + "nullable": true + }, + "requestBody": { + "type": "string", + "description": "Request body content for POST/PUT/PATCH methods", + "nullable": true + }, + "contentType": { + "type": "string", + "description": "Content-Type header value for the request body", + "nullable": true + }, + "verifyTls": { + "type": "boolean", + "description": "Whether to verify TLS certificates (default: true)", + "nullable": true } - ] + } }, "Icmp": { "required": [ @@ -24453,38 +24418,27 @@ "host" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorConfig" + "properties": { + "host": { + "minLength": 1, + "type": "string", + "description": "Target hostname or IP address to ping" }, - { - "type": "object", - "properties": { - "host": { - "minLength": 1, - "type": "string", - "description": "Target hostname or IP address to ping" - }, - "packetCount": { - "maximum": 20, - "minimum": 1, - "type": "integer", - "description": "Number of ICMP packets to send", - "format": "int32", - "nullable": true - }, - "timeoutMs": { - "type": "integer", - "description": "Ping timeout in milliseconds", - "format": "int32", - "nullable": true - } - }, - "required": [ - "host" - ] + "packetCount": { + "maximum": 20, + "minimum": 1, + "type": "integer", + "description": "Number of ICMP packets to send", + "format": "int32", + "nullable": true + }, + "timeoutMs": { + "type": "integer", + "description": "Ping timeout in milliseconds", + "format": "int32", + "nullable": true } - ] + } }, "IcmpPacketLossAssertion": { "type": "object", @@ -25686,44 +25640,33 @@ "command" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorConfig" + "properties": { + "command": { + "minLength": 1, + "type": "string", + "description": "Command to execute to start the MCP server" }, - { + "args": { + "type": "array", + "description": "Command-line arguments for the MCP server process", + "nullable": true, + "items": { + "type": "string", + "description": "Command-line arguments for the MCP server process", + "nullable": true + } + }, + "env": { "type": "object", - "properties": { - "command": { - "minLength": 1, - "type": "string", - "description": "Command to execute to start the MCP server" - }, - "args": { - "type": "array", - "description": "Command-line arguments for the MCP server process", - "nullable": true, - "items": { - "type": "string", - "description": "Command-line arguments for the MCP server process", - "nullable": true - } - }, - "env": { - "type": "object", - "additionalProperties": { - "type": "string", - "description": "Environment variables to pass to the MCP server process", - "nullable": true - }, - "description": "Environment variables to pass to the MCP server process", - "nullable": true - } + "additionalProperties": { + "type": "string", + "description": "Environment variables to pass to the MCP server process", + "nullable": true }, - "required": [ - "command" - ] + "description": "Environment variables to pass to the MCP server process", + "nullable": true } - ] + } }, "McpToolAvailableAssertion": { "required": [ @@ -26092,10 +26035,6 @@ } } }, - "MonitorConfig": { - "type": "object", - "description": "Protocol-specific monitor configuration; concrete type is deduced from the JSON shape" - }, "MonitorDto": { "required": [ "config", @@ -31326,41 +31265,30 @@ }, "TcpMonitorConfig": { "required": [ - "host" + "host", + "port" ], "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorConfig" + "properties": { + "host": { + "minLength": 1, + "type": "string", + "description": "Target hostname or IP address" }, - { - "type": "object", - "properties": { - "host": { - "minLength": 1, - "type": "string", - "description": "Target hostname or IP address" - }, - "port": { - "maximum": 65535, - "minimum": 1, - "type": "integer", - "description": "TCP port to connect to", - "format": "int32" - }, - "timeoutMs": { - "type": "integer", - "description": "Connection timeout in milliseconds", - "format": "int32", - "nullable": true - } - }, - "required": [ - "host", - "port" - ] + "port": { + "maximum": 65535, + "minimum": 1, + "type": "integer", + "description": "TCP port to connect to", + "format": "int32" + }, + "timeoutMs": { + "type": "integer", + "description": "Connection timeout in milliseconds", + "format": "int32", + "nullable": true } - ] + } }, "TcpResponseTimeAssertion": { "type": "object", @@ -32046,9 +31974,24 @@ }, "config": { "nullable": true, - "allOf": [ + "oneOf": [ + { + "$ref": "#/components/schemas/DnsMonitorConfig" + }, { - "$ref": "#/components/schemas/MonitorConfig" + "$ref": "#/components/schemas/HeartbeatMonitorConfig" + }, + { + "$ref": "#/components/schemas/HttpMonitorConfig" + }, + { + "$ref": "#/components/schemas/IcmpMonitorConfig" + }, + { + "$ref": "#/components/schemas/McpServerMonitorConfig" + }, + { + "$ref": "#/components/schemas/TcpMonitorConfig" } ] }, diff --git a/src/devhelm/_generated.py b/src/devhelm/_generated.py index a2a3d03..6db3fcb 100644 --- a/src/devhelm/_generated.py +++ b/src/devhelm/_generated.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: .openapi-preprocessed.json -# timestamp: 2026-04-20T14:50:16+00:00 +# timestamp: 2026-04-20T17:00:30+00:00 from __future__ import annotations from typing import Annotated, Any, Literal @@ -1481,6 +1481,34 @@ class RecordType(StrEnum): ptr = "PTR" +class DnsMonitorConfig(BaseModel): + hostname: Annotated[str, Field(description="Domain name to resolve", min_length=1)] + record_types: Annotated[ + list[RecordType] | None, + Field( + alias="recordTypes", + description="DNS record types to query: A, AAAA, CNAME, MX, NS, TXT, SRV, SOA, CAA, PTR", + ), + ] = None + nameservers: Annotated[ + list[str] | None, + Field( + description="Custom nameservers to query (uses system defaults if omitted)" + ), + ] = None + timeout_ms: Annotated[ + int | None, + Field(alias="timeoutMs", description="Per-query timeout in milliseconds"), + ] = None + total_timeout_ms: Annotated[ + int | None, + Field( + alias="totalTimeoutMs", + description="Total timeout for all queries in milliseconds", + ), + ] = None + + class Type11(StrEnum): dns_record_contains = "dns_record_contains" @@ -1846,6 +1874,26 @@ class HeartbeatMaxIntervalAssertion(BaseModel): ] +class HeartbeatMonitorConfig(BaseModel): + expected_interval: Annotated[ + int, + Field( + alias="expectedInterval", + description="Expected heartbeat interval in seconds", + ge=1, + le=86400, + ), + ] + grace_period: Annotated[ + int, + Field( + alias="gracePeriod", + description="Grace period in seconds before marking as down", + ge=1, + ), + ] + + class Type23(StrEnum): heartbeat_payload_contains = "heartbeat_payload_contains" @@ -1906,6 +1954,43 @@ class Method(StrEnum): head = "HEAD" +class HttpMonitorConfig(BaseModel): + url: Annotated[ + str, Field(description="Target URL to send requests to", min_length=1) + ] + method: Annotated[ + Method, Field(description="HTTP method: GET, POST, PUT, PATCH, DELETE, or HEAD") + ] + custom_headers: Annotated[ + dict[str, str] | None, + Field( + alias="customHeaders", + description="Additional HTTP headers to include in requests", + ), + ] = None + request_body: Annotated[ + str | None, + Field( + alias="requestBody", + description="Request body content for POST/PUT/PATCH methods", + ), + ] = None + content_type: Annotated[ + str | None, + Field( + alias="contentType", + description="Content-Type header value for the request body", + ), + ] = None + verify_tls: Annotated[ + bool | None, + Field( + alias="verifyTls", + description="Whether to verify TLS certificates (default: true)", + ), + ] = None + + class CheckType2(StrEnum): icmp = "icmp" @@ -1942,6 +2027,24 @@ class Icmp(BaseModel): ] = None +class IcmpMonitorConfig(BaseModel): + host: Annotated[ + str, Field(description="Target hostname or IP address to ping", min_length=1) + ] + packet_count: Annotated[ + int | None, + Field( + alias="packetCount", + description="Number of ICMP packets to send", + ge=1, + le=20, + ), + ] = None + timeout_ms: Annotated[ + int | None, Field(alias="timeoutMs", description="Ping timeout in milliseconds") + ] = None + + class Type25(StrEnum): icmp_packet_loss = "icmp_packet_loss" @@ -2631,6 +2734,21 @@ class McpServer(BaseModel): ] = None +class McpServerMonitorConfig(BaseModel): + command: Annotated[ + str, + Field(description="Command to execute to start the MCP server", min_length=1), + ] + args: Annotated[ + list[str] | None, + Field(description="Command-line arguments for the MCP server process"), + ] = None + env: Annotated[ + dict[str, str] | None, + Field(description="Environment variables to pass to the MCP server process"), + ] = None + + class Type36(StrEnum): mcp_tool_available = "mcp_tool_available" @@ -2729,10 +2847,6 @@ class MonitorAuthDto(BaseModel): config: ApiKeyAuthConfig | BasicAuthConfig | BearerAuthConfig | HeaderAuthConfig -class MonitorConfig(BaseModel): - pass - - class Type38(StrEnum): http = "HTTP" dns = "DNS" @@ -4761,7 +4875,7 @@ class TcpConnectsAssertion(BaseModel): type: Type49 -class TcpMonitorConfig(MonitorConfig): +class TcpMonitorConfig(BaseModel): host: Annotated[ str, Field(description="Target hostname or IP address", min_length=1) ] @@ -5915,6 +6029,69 @@ class CreateAssertionRequest(BaseModel): ] = None +class CreateMonitorRequest(BaseModel): + name: Annotated[ + str, + Field( + description="Human-readable name for this monitor", + max_length=255, + min_length=0, + ), + ] + type: Annotated[Type5, Field(description="Monitor protocol type")] + config: ( + DnsMonitorConfig + | HeartbeatMonitorConfig + | HttpMonitorConfig + | IcmpMonitorConfig + | McpServerMonitorConfig + | TcpMonitorConfig + ) + frequency_seconds: Annotated[ + int | None, + Field( + alias="frequencySeconds", + description="Check frequency in seconds (30–86400); null defaults to plan minimum (60s on most paid plans)", + ), + ] = None + enabled: Annotated[ + bool | None, Field(description="Whether the monitor is active (default: true)") + ] = None + regions: Annotated[ + list[str] | None, + Field(description="Probe regions to run checks from, e.g. us-east, eu-west"), + ] = None + managed_by: Annotated[ + ManagedBy, + Field( + alias="managedBy", description="Who manages this monitor: DASHBOARD or CLI" + ), + ] + environment_id: Annotated[ + UUID | None, + Field( + alias="environmentId", + description="Environment to associate with this monitor", + ), + ] = None + assertions: Annotated[ + list[CreateAssertionRequest] | None, + Field(description="Assertions to evaluate against each check result"), + ] = None + auth: MonitorAuthConfig | None = None + incident_policy: Annotated[ + UpdateIncidentPolicyRequest | None, Field(alias="incidentPolicy") + ] = None + alert_channel_ids: Annotated[ + list[UUID] | None, + Field( + alias="alertChannelIds", + description="Alert channels to notify when this monitor triggers", + ), + ] = None + tags: AddMonitorTagsRequest | None = None + + class CreateResourceGroupRequest(BaseModel): name: Annotated[ str, @@ -6092,34 +6269,6 @@ class DashboardOverviewDto(BaseModel): incidents: IncidentsSummaryDto -class DnsMonitorConfig(MonitorConfig): - hostname: Annotated[str, Field(description="Domain name to resolve", min_length=1)] - record_types: Annotated[ - list[RecordType] | None, - Field( - alias="recordTypes", - description="DNS record types to query: A, AAAA, CNAME, MX, NS, TXT, SRV, SOA, CAA, PTR", - ), - ] = None - nameservers: Annotated[ - list[str] | None, - Field( - description="Custom nameservers to query (uses system defaults if omitted)" - ), - ] = None - timeout_ms: Annotated[ - int | None, - Field(alias="timeoutMs", description="Per-query timeout in milliseconds"), - ] = None - total_timeout_ms: Annotated[ - int | None, - Field( - alias="totalTimeoutMs", - description="Total timeout for all queries in milliseconds", - ), - ] = None - - class EscalationChain(BaseModel): steps: Annotated[ list[EscalationStep], @@ -6200,81 +6349,6 @@ class GlobalStatusSummaryDto(BaseModel): ] -class HeartbeatMonitorConfig(MonitorConfig): - expected_interval: Annotated[ - int, - Field( - alias="expectedInterval", - description="Expected heartbeat interval in seconds", - ge=1, - le=86400, - ), - ] - grace_period: Annotated[ - int, - Field( - alias="gracePeriod", - description="Grace period in seconds before marking as down", - ge=1, - ), - ] - - -class HttpMonitorConfig(MonitorConfig): - url: Annotated[ - str, Field(description="Target URL to send requests to", min_length=1) - ] - method: Annotated[ - Method, Field(description="HTTP method: GET, POST, PUT, PATCH, DELETE, or HEAD") - ] - custom_headers: Annotated[ - dict[str, str] | None, - Field( - alias="customHeaders", - description="Additional HTTP headers to include in requests", - ), - ] = None - request_body: Annotated[ - str | None, - Field( - alias="requestBody", - description="Request body content for POST/PUT/PATCH methods", - ), - ] = None - content_type: Annotated[ - str | None, - Field( - alias="contentType", - description="Content-Type header value for the request body", - ), - ] = None - verify_tls: Annotated[ - bool | None, - Field( - alias="verifyTls", - description="Whether to verify TLS certificates (default: true)", - ), - ] = None - - -class IcmpMonitorConfig(MonitorConfig): - host: Annotated[ - str, Field(description="Target hostname or IP address to ping", min_length=1) - ] - packet_count: Annotated[ - int | None, - Field( - alias="packetCount", - description="Number of ICMP packets to send", - ge=1, - le=20, - ), - ] = None - timeout_ms: Annotated[ - int | None, Field(alias="timeoutMs", description="Ping timeout in milliseconds") - ] = None - - class IncidentDetailDto(BaseModel): incident: IncidentDto updates: list[IncidentUpdateDto] @@ -6342,21 +6416,6 @@ class IntegrationDto(BaseModel): config_schema: Annotated[IntegrationConfigSchemaDto, Field(alias="configSchema")] -class McpServerMonitorConfig(MonitorConfig): - command: Annotated[ - str, - Field(description="Command to execute to start the MCP server", min_length=1), - ] - args: Annotated[ - list[str] | None, - Field(description="Command-line arguments for the MCP server process"), - ] = None - env: Annotated[ - dict[str, str] | None, - Field(description="Environment variables to pass to the MCP server process"), - ] = None - - class MonitorAssertionDto(BaseModel): id: UUID monitor_id: Annotated[UUID, Field(alias="monitorId")] @@ -7000,7 +7059,15 @@ class UpdateMonitorRequest(BaseModel): min_length=0, ), ] = None - config: MonitorConfig | None = None + config: ( + DnsMonitorConfig + | HeartbeatMonitorConfig + | HttpMonitorConfig + | IcmpMonitorConfig + | McpServerMonitorConfig + | TcpMonitorConfig + | None + ) = None frequency_seconds: Annotated[ int | None, Field( @@ -7184,69 +7251,6 @@ class CheckResultDto(BaseModel): ] = None -class CreateMonitorRequest(BaseModel): - name: Annotated[ - str, - Field( - description="Human-readable name for this monitor", - max_length=255, - min_length=0, - ), - ] - type: Annotated[Type5, Field(description="Monitor protocol type")] - config: ( - DnsMonitorConfig - | HeartbeatMonitorConfig - | HttpMonitorConfig - | IcmpMonitorConfig - | McpServerMonitorConfig - | TcpMonitorConfig - ) - frequency_seconds: Annotated[ - int | None, - Field( - alias="frequencySeconds", - description="Check frequency in seconds (30–86400); null defaults to plan minimum (60s on most paid plans)", - ), - ] = None - enabled: Annotated[ - bool | None, Field(description="Whether the monitor is active (default: true)") - ] = None - regions: Annotated[ - list[str] | None, - Field(description="Probe regions to run checks from, e.g. us-east, eu-west"), - ] = None - managed_by: Annotated[ - ManagedBy, - Field( - alias="managedBy", description="Who manages this monitor: DASHBOARD or CLI" - ), - ] - environment_id: Annotated[ - UUID | None, - Field( - alias="environmentId", - description="Environment to associate with this monitor", - ), - ] = None - assertions: Annotated[ - list[CreateAssertionRequest] | None, - Field(description="Assertions to evaluate against each check result"), - ] = None - auth: MonitorAuthConfig | None = None - incident_policy: Annotated[ - UpdateIncidentPolicyRequest | None, Field(alias="incidentPolicy") - ] = None - alert_channel_ids: Annotated[ - list[UUID] | None, - Field( - alias="alertChannelIds", - description="Alert channels to notify when this monitor triggers", - ), - ] = None - tags: AddMonitorTagsRequest | None = None - - class CreateNotificationPolicyRequest(BaseModel): name: Annotated[ str,