{
  "openapi": "3.0.1",
  "info": {
    "title": "City of Alpenglow Developer API",
    "version": "2.1.0",
    "description": "Public REST API for the fictional City of Alpenglow, CO: permit records, business licenses, and parks & facilities, each served by its own microservice. v2 is current; v1 still works but answers with Deprecation/Sunset headers pointing at its successor. All endpoints except /v2/status and /v1/ping require an x-api-key header; the shared demo key is printed at https://api.demos.planetek.org.",
    "contact": {
      "name": "Planetek",
      "url": "https://demos.planetek.org"
    }
  },
  "tags": [
    {
      "name": "permits",
      "description": "Permits service: building/electrical/plumbing/etc. permit records, plus inspection requests (the only write surface: schema-validated by the gateway, auto-expiring after 24h)."
    },
    {
      "name": "licenses",
      "description": "Licenses service: active and historical business licenses."
    },
    {
      "name": "facilities",
      "description": "Facilities service: parks, trails, rec centers, libraries, pools, and their hours. v2-only, since it launched after the v1 deprecation."
    },
    {
      "name": "exports",
      "description": "Exports service: asynchronous full-catalog exports. POST answers 202 with a Location header to poll; a finished job hands back a presigned download URL that expires after 15 minutes. The async-job pattern for work that outlives one request."
    },
    {
      "name": "platform",
      "description": "Keyless platform endpoints: live status, the party line's usage meter, self-service key issuance, and a mock-integration ping."
    }
  ],
  "paths": {
    "/v1/ping": {
      "get": {
        "tags": [
          "platform"
        ],
        "summary": "Ping (mock integration)",
        "description": "Answered entirely by an API Gateway mock integration. No compute behind it at all.",
        "responses": {
          "200": {
            "description": "Pong.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ping"
                }
              }
            }
          }
        }
      }
    },
    "/v1/permits": {
      "get": {
        "tags": [
          "permits"
        ],
        "summary": "List permits (deprecated)",
        "deprecated": true,
        "description": "Bare-array response, page capped at 50, no cursor. Replies carry Deprecation, Sunset, and Link successor-version headers. Use GET /v2/permits.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/PermitType"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/PermitStatus"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Up to 50 permits, no pagination.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Permit"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/permits/{id}": {
      "get": {
        "tags": [
          "permits"
        ],
        "summary": "Get a permit (deprecated)",
        "deprecated": true,
        "description": "Bare-object response with deprecation headers. Use GET /v2/permits/{id}.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "PRM-2025-0104"
          }
        ],
        "responses": {
          "200": {
            "description": "The permit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Permit"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/licenses": {
      "get": {
        "tags": [
          "licenses"
        ],
        "summary": "List business licenses (deprecated)",
        "deprecated": true,
        "description": "Bare-array response with deprecation headers. Use GET /v2/licenses.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/LicenseCategory"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/LicenseStatus"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Up to 50 licenses, no pagination.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/License"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v2/status": {
      "get": {
        "tags": [
          "platform"
        ],
        "summary": "Platform status",
        "description": "Keyless. Live health + approximate catalog size per microservice, read from DynamoDB table metadata only.",
        "responses": {
          "200": {
            "description": "Per-service status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlatformStatus"
                }
              }
            }
          }
        }
      }
    },
    "/v2/platform/usage": {
      "get": {
        "tags": [
          "platform"
        ],
        "summary": "Party-line usage meter",
        "description": "Keyless. Live metering of the shared demo key, read from the same usage-plan counters that enforce its quota, plus how many personal visitor keys the exchange has issued today. The meter can lag live traffic by a few minutes.",
        "responses": {
          "200": {
            "description": "Today's metering.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PlanUsage"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v2/platform/keys": {
      "post": {
        "tags": [
          "platform"
        ],
        "summary": "Issue yourself a personal API key",
        "description": "Keyless, because you cannot present a key you do not have yet. Mints a real API key on the visitor usage plan (2 req/s, burst 5, 500 requests/day of your own) and returns it exactly once. Keys expire about 24 hours after issue and are removed by the nightly sweep. Issuance is capped per address and per day; the body is validated by the gateway against the KeyRequest model before any code runs.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KeyRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Your personal key — shown once and never again.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/IssuedKey"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body failed gateway schema validation. The Lambda never ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Issuance cap reached (per address or exchange-wide) — the shared demo key still works.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v2/exports": {
      "post": {
        "tags": [
          "exports"
        ],
        "summary": "Start a catalog export (async)",
        "description": "The asynchronous-job pattern. Answers 202 Accepted immediately with a Location header pointing at the job resource; the export itself runs on a queue-fed worker. Poll the Location URL until status is done, then follow the presigned downloadUrl (valid 15 minutes). The body is validated by the gateway against the ExportRequest model.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExportRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job accepted; poll the Location header.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                },
                "description": "The job resource to poll, e.g. /v2/exports/EXP-ABC123."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ExportJob"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body failed gateway schema validation. The Lambda never ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Daily export allotment spent; resets at midnight UTC.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v2/exports/{id}": {
      "get": {
        "tags": [
          "exports"
        ],
        "summary": "Poll an export job",
        "description": "Job status; once done, the response includes a presigned downloadUrl that expires after 15 minutes.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "EXP-ABC123XY"
          }
        ],
        "responses": {
          "200": {
            "description": "The job, enveloped. downloadUrl present only when status is done.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ExportJob"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v2/permits": {
      "get": {
        "tags": [
          "permits"
        ],
        "summary": "List permits",
        "description": "Cursor-paginated envelope. Filter with ?type= and ?status=; page with ?limit= (max 50) and the opaque meta.nextToken from the previous page.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/PermitType"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/PermitStatus"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          },
          {
            "name": "nextToken",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque cursor from the previous page's meta.nextToken."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of permits.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Permit"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v2/permits/{id}": {
      "get": {
        "tags": [
          "permits"
        ],
        "summary": "Get a permit",
        "description": "Responses carry a strong ETag; repeat the call with If-None-Match and an unchanged record answers 304 with no body.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "PRM-2025-0104"
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "ETag from a previous response; matching answers 304."
          }
        ],
        "responses": {
          "200": {
            "description": "The permit, enveloped.",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                },
                "description": "Strong validator for If-None-Match on later calls."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Permit"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v2/permits/{id}/inspections": {
      "get": {
        "tags": [
          "permits"
        ],
        "summary": "List inspection requests for a permit",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "PRM-2025-0104"
          }
        ],
        "responses": {
          "200": {
            "description": "Inspection requests currently on file (visitor-created ones expire after 24h).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Inspection"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "permits"
        ],
        "summary": "Request an inspection",
        "description": "The write surface, and the request-validation exhibit. API Gateway checks the body against the InspectionRequest model (required fields, enums, formats, no unknown properties) and rejects invalid payloads with a 400 before any Lambda runs. Created records expire automatically after 24 hours. Only approved or issued permits accept inspection requests (409 otherwise). Send an Idempotency-Key header and retrying the identical request within 24 hours replays the original 201 (marked Idempotency-Replayed: true) instead of creating a duplicate.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "PRM-2025-0104"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            },
            "description": "Optional client-chosen key (1-64 chars) that makes this POST safely retryable for 24 hours."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InspectionRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Inspection request created (expires in 24h). A replayed response carries Idempotency-Replayed true.",
            "headers": {
              "Idempotency-Replayed": {
                "schema": {
                  "type": "string"
                },
                "description": "Present (true) when this 201 is the stored original, replayed for a repeated Idempotency-Key."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Inspection"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body failed gateway schema validation. The Lambda never ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Permit exists but is not in an inspectable status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v2/licenses": {
      "get": {
        "tags": [
          "licenses"
        ],
        "summary": "List business licenses",
        "description": "Cursor-paginated envelope. Filter with ?category= and ?status=.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/LicenseCategory"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/LicenseStatus"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          },
          {
            "name": "nextToken",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of licenses.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/License"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v2/licenses/{id}": {
      "get": {
        "tags": [
          "licenses"
        ],
        "summary": "Get a business license",
        "description": "Responses carry a strong ETag; repeat the call with If-None-Match and an unchanged record answers 304 with no body.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "LIC-0042"
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "ETag from a previous response; matching answers 304."
          }
        ],
        "responses": {
          "200": {
            "description": "The license, enveloped.",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                },
                "description": "Strong validator for If-None-Match on later calls."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/License"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v2/facilities": {
      "get": {
        "tags": [
          "facilities"
        ],
        "summary": "List facilities",
        "description": "Cursor-paginated envelope. Filter with ?kind=. Hours are on the /hours sub-resource.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/FacilityKind"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          },
          {
            "name": "nextToken",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of facilities.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Facility"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v2/facilities/{id}": {
      "get": {
        "tags": [
          "facilities"
        ],
        "summary": "Get a facility",
        "description": "Responses carry a strong ETag; repeat the call with If-None-Match and an unchanged record answers 304 with no body.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "FAC-009"
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "ETag from a previous response; matching answers 304."
          }
        ],
        "responses": {
          "200": {
            "description": "The facility, enveloped.",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                },
                "description": "Strong validator for If-None-Match on later calls."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Facility"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v2/facilities/{id}/hours": {
      "get": {
        "tags": [
          "facilities"
        ],
        "summary": "Get a facility's hours",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "FAC-009"
          }
        ],
        "responses": {
          "200": {
            "description": "Weekly hours plus any seasonal note.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/FacilityHours"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "name": "x-api-key",
        "in": "header",
        "description": "Usage-plan API key. The shared demo-tier key is printed on the docs page; partner-tier keys are issued per integrator."
      }
    },
    "responses": {
      "NotFound": {
        "description": "No record with that id.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotModified": {
        "description": "Not modified — the ETag presented in If-None-Match still matches, so no body is sent."
      }
    },
    "schemas": {
      "PermitType": {
        "type": "string",
        "enum": [
          "building",
          "electrical",
          "plumbing",
          "mechanical",
          "sign",
          "fence",
          "solar",
          "event"
        ]
      },
      "PermitStatus": {
        "type": "string",
        "enum": [
          "submitted",
          "under-review",
          "approved",
          "issued",
          "denied",
          "closed"
        ]
      },
      "LicenseCategory": {
        "type": "string",
        "enum": [
          "food-service",
          "retail",
          "contractor",
          "childcare",
          "liquor",
          "lodging",
          "mobile-vendor"
        ]
      },
      "LicenseStatus": {
        "type": "string",
        "enum": [
          "active",
          "expired",
          "suspended"
        ]
      },
      "FacilityKind": {
        "type": "string",
        "enum": [
          "park",
          "trail",
          "rec-center",
          "library",
          "pool",
          "sports-field"
        ]
      },
      "Permit": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "PRM-2025-0104"
          },
          "type": {
            "$ref": "#/components/schemas/PermitType"
          },
          "status": {
            "$ref": "#/components/schemas/PermitStatus"
          },
          "description": {
            "type": "string",
            "example": "200A service upgrade"
          },
          "address": {
            "type": "string",
            "example": "1420 Larkspur Ave"
          },
          "applicant": {
            "type": "string",
            "example": "Peak & Pine Electric"
          },
          "valuation": {
            "type": "integer",
            "description": "USD",
            "example": 12400
          },
          "submittedAt": {
            "type": "string",
            "example": "2025-03-11"
          }
        }
      },
      "License": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "LIC-0042"
          },
          "businessName": {
            "type": "string",
            "example": "Powderhound Coffee"
          },
          "category": {
            "$ref": "#/components/schemas/LicenseCategory"
          },
          "status": {
            "$ref": "#/components/schemas/LicenseStatus"
          },
          "address": {
            "type": "string",
            "example": "210 Cirque St"
          },
          "issuedAt": {
            "type": "string",
            "example": "2024-05-02"
          },
          "expiresAt": {
            "type": "string",
            "example": "2026-05-02"
          }
        }
      },
      "Facility": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "FAC-009"
          },
          "name": {
            "type": "string",
            "example": "Cirque Recreation Center"
          },
          "kind": {
            "$ref": "#/components/schemas/FacilityKind"
          },
          "address": {
            "type": "string",
            "example": "800 Summit Way"
          },
          "status": {
            "type": "string",
            "example": "open"
          }
        }
      },
      "FacilityHours": {
        "type": "object",
        "properties": {
          "facilityId": {
            "type": "string",
            "example": "FAC-009"
          },
          "name": {
            "type": "string",
            "example": "Cirque Recreation Center"
          },
          "hours": {
            "type": "object",
            "description": "mon…sun → opening hours or 'closed'",
            "additionalProperties": {
              "type": "string"
            }
          },
          "seasonalNote": {
            "type": "string"
          }
        }
      },
      "InspectionRequest": {
        "type": "object",
        "description": "Enforced by API Gateway's request validator, not by application code.",
        "required": [
          "type",
          "preferredDate",
          "contactEmail"
        ],
        "additionalProperties": false,
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "footing",
              "rough",
              "electrical-service",
              "plumbing-top-out",
              "final"
            ]
          },
          "preferredDate": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "ISO date (YYYY-MM-DD)."
          },
          "contactEmail": {
            "type": "string",
            "pattern": "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"
          },
          "notes": {
            "type": "string",
            "maxLength": 500
          }
        }
      },
      "Inspection": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "INSP-LZK4A2M01"
          },
          "permitId": {
            "type": "string",
            "example": "PRM-2025-0104"
          },
          "type": {
            "type": "string",
            "example": "rough"
          },
          "preferredDate": {
            "type": "string",
            "example": "2026-08-01"
          },
          "contactEmail": {
            "type": "string",
            "example": "inspector@example.com"
          },
          "notes": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "example": "requested"
          },
          "requestedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "KeyRequest": {
        "type": "object",
        "description": "Enforced by API Gateway's request validator, not by application code.",
        "required": [
          "label"
        ],
        "additionalProperties": false,
        "properties": {
          "label": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{2,39}$",
            "description": "A short name for your key (3-40 chars, lowercase letters, digits, hyphens). It becomes part of the key's name on the exchange."
          }
        }
      },
      "IssuedKey": {
        "type": "object",
        "properties": {
          "apiKey": {
            "type": "string",
            "description": "The key value — shown once and never retrievable again."
          },
          "keyId": {
            "type": "string"
          },
          "label": {
            "type": "string",
            "example": "my-integration"
          },
          "plan": {
            "type": "string",
            "example": "visitor"
          },
          "limits": {
            "type": "object",
            "properties": {
              "ratePerSecond": {
                "type": "integer",
                "example": 2
              },
              "burst": {
                "type": "integer",
                "example": 5
              },
              "quotaPerDay": {
                "type": "integer",
                "example": 500
              }
            }
          },
          "issuedAt": {
            "type": "string",
            "format": "date-time"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "note": {
            "type": "string"
          }
        }
      },
      "PlanUsage": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "example": "2026-07-29"
          },
          "partyLine": {
            "type": "object",
            "description": "The shared demo key, as metered by its usage plan.",
            "properties": {
              "plan": {
                "type": "string",
                "example": "demo"
              },
              "used": {
                "type": "integer",
                "example": 412
              },
              "remaining": {
                "type": "integer",
                "example": 2088
              },
              "quota": {
                "type": "integer",
                "example": 2500
              },
              "resets": {
                "type": "string",
                "example": "midnight UTC"
              }
            }
          },
          "privateLines": {
            "type": "object",
            "description": "Self-issued visitor keys.",
            "properties": {
              "plan": {
                "type": "string",
                "example": "visitor"
              },
              "issuedToday": {
                "type": "integer",
                "example": 3
              },
              "dailyCap": {
                "type": "integer",
                "example": 25
              },
              "quotaPerKey": {
                "type": "integer",
                "example": 500
              }
            }
          },
          "note": {
            "type": "string"
          }
        }
      },
      "ExportRequest": {
        "type": "object",
        "description": "Enforced by API Gateway's request validator, not by application code.",
        "required": [
          "service",
          "format"
        ],
        "additionalProperties": false,
        "properties": {
          "service": {
            "type": "string",
            "enum": [
              "permits",
              "licenses",
              "facilities"
            ],
            "description": "Which catalog to export."
          },
          "format": {
            "type": "string",
            "enum": [
              "json",
              "csv"
            ]
          }
        }
      },
      "ExportJob": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "EXP-ABC123XY"
          },
          "service": {
            "type": "string",
            "example": "facilities"
          },
          "format": {
            "type": "string",
            "example": "json"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "done",
              "failed"
            ],
            "example": "done"
          },
          "requestedAt": {
            "type": "string",
            "format": "date-time"
          },
          "startedAt": {
            "type": "string",
            "format": "date-time"
          },
          "completedAt": {
            "type": "string",
            "format": "date-time"
          },
          "count": {
            "type": "integer",
            "description": "Records in the finished export."
          },
          "sizeBytes": {
            "type": "integer"
          },
          "downloadUrl": {
            "type": "string",
            "description": "Presigned S3 URL; present only while status is done",
            "valid 15 minutes from the poll that returned it.": null
          },
          "downloadExpiresSeconds": {
            "type": "integer",
            "example": 900
          },
          "error": {
            "type": "string"
          }
        }
      },
      "PageMeta": {
        "type": "object",
        "properties": {
          "count": {
            "type": "integer",
            "description": "Items in this page."
          },
          "nextToken": {
            "type": "string",
            "description": "Opaque cursor for the next page; absent on the last page."
          }
        }
      },
      "Ping": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "example": "pong"
          },
          "integration": {
            "type": "string",
            "example": "mock"
          },
          "note": {
            "type": "string"
          }
        }
      },
      "PlatformStatus": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "example": "operational"
          },
          "apiVersions": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "services": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "example": "permits"
                },
                "status": {
                  "type": "string",
                  "example": "operational"
                },
                "approximateRecords": {
                  "type": "integer",
                  "example": 240
                }
              }
            }
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "example": "not_found"
          },
          "message": {
            "type": "string"
          }
        }
      }
    }
  },
  "servers": [
    {
      "url": "https://api.demos.planetek.org"
    }
  ]
}