{
  "openapi": "3.1.0",
  "info": {
    "title": "PARKENA Operator API",
    "version": "v1",
    "summary": "Push your car parks and your prices; pull your PARKENA bookings.",
    "description": "The v1 operator ingest API. Generated from the code that serves it (supabase/functions/_shared/api-openapi.ts); the long-form reference, with every rule explained, is https://parkena.com/operators/api. Every refusal of a credential is the same 401 -- use GET /ping to check one. There is no sandbox: ?dry_run=true on the three PUT routes rehearses a write against your real data and rolls it back. Rate limits are listed in x-parkena-rate-limits; each operation names the per-credential buckets it spends (x-parkena-rate-limits, and x-parkena-dry-run-rate-limits on the writes). Every bucket is counted in the memory of the edge worker serving the request (held_in): per worker, not shared between workers, and reset when a worker is replaced -- at most every 400 seconds on the hosted platform -- so the hourly write ceiling bounds one worker, not a clock hour. A refused request spends no token from any bucket.",
    "contact": {
      "name": "PARKENA",
      "email": "info@parkena.com",
      "url": "https://parkena.com/operators/api"
    }
  },
  "externalDocs": {
    "description": "The full reference",
    "url": "https://parkena.com/operators/api"
  },
  "servers": [
    {
      "url": "https://api.parkena.com/v1",
      "description": "Production. There is no test base."
    }
  ],
  "security": [
    {
      "credential": []
    }
  ],
  "paths": {
    "/ping": {
      "get": {
        "operationId": "ping",
        "summary": "Check a credential, and see what it may do",
        "description": "The route to use when an integration is not working: every other refusal is deliberately unable to say which of six things went wrong. Establishes no operator context and reads no rows.",
        "security": [
          {
            "credential": []
          }
        ],
        "x-parkena-scope": null,
        "x-parkena-rate-limits": [
          "credential.read"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ping"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "422": {
            "$ref": "#/components/responses/Error422"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    },
    "/lots": {
      "get": {
        "operationId": "listLots",
        "summary": "Your car parks, each with its PARKENA price envelope",
        "description": "Keyset-paged on your own external_id, ascending. Follow next_after until it is null. rates is null for a car park with no PARKENA price yet.",
        "security": [
          {
            "credential": [
              "read_supply"
            ]
          }
        ],
        "x-parkena-scope": "read_supply",
        "x-parkena-rate-limits": [
          "credential.read"
        ],
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "Resume after this external_id: the next_after of the previous page. Not empty.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Asking for more than the maximum is refused (invalid_query), never reduced.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LotPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "422": {
            "$ref": "#/components/responses/Error422"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    },
    "/lots/{external_id}": {
      "put": {
        "operationId": "putLot",
        "summary": "Create or replace one car park",
        "description": "A FULL representation: an absent key means null, not \"leave it as it was\". Diff before update: a push that changes nothing issues no statement and answers \"unchanged\". Changing a reviewed field voids your listing review (listing_review_superseded). With ?dry_run=true the write runs, is reported, and is rolled back.",
        "security": [
          {
            "credential": [
              "write_supply"
            ]
          }
        ],
        "x-parkena-scope": "write_supply",
        "x-parkena-rate-limits": [
          "credential.write.hourly",
          "credential.write"
        ],
        "x-parkena-dry-run-rate-limits": [
          "credential.write"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/external_id_upsert"
          },
          {
            "$ref": "#/components/parameters/dry_run"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LotBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated or unchanged -- and EVERY dry run, whatever it reports.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LotWriteResult"
                }
              }
            }
          },
          "201": {
            "description": "Created. Never the answer to a dry run.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LotWriteResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "409": {
            "$ref": "#/components/responses/Error409"
          },
          "413": {
            "$ref": "#/components/responses/Error413"
          },
          "415": {
            "$ref": "#/components/responses/Error415"
          },
          "422": {
            "$ref": "#/components/responses/Error422"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    },
    "/lots/{external_id}/rates": {
      "put": {
        "operationId": "putLotRates",
        "summary": "Set one car park's PARKENA price envelope",
        "description": "Writes the parkena channel and only that channel. Either a range (floor, base, ceiling) or one fixed_price. envelope_widened is true when the envelope moved outward, which is what can cost a listing review. With ?dry_run=true the write runs, is reported, and is rolled back.",
        "security": [
          {
            "credential": [
              "write_supply"
            ]
          }
        ],
        "x-parkena-scope": "write_supply",
        "x-parkena-rate-limits": [
          "credential.write.hourly",
          "credential.write"
        ],
        "x-parkena-dry-run-rate-limits": [
          "credential.write"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/external_id"
          },
          {
            "$ref": "#/components/parameters/dry_run"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RatesBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated or unchanged -- and EVERY dry run, whatever it reports.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RatesWriteResult"
                }
              }
            }
          },
          "201": {
            "description": "Created. Never the answer to a dry run.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RatesWriteResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "409": {
            "$ref": "#/components/responses/Error409"
          },
          "413": {
            "$ref": "#/components/responses/Error413"
          },
          "415": {
            "$ref": "#/components/responses/Error415"
          },
          "422": {
            "$ref": "#/components/responses/Error422"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    },
    "/lots/{external_id}/blocked-periods": {
      "get": {
        "operationId": "listBlockedPeriods",
        "summary": "One car park's closure list",
        "description": "Every date range the car park is withdrawn from PARKENA sale, whoever authored it -- your syncs and the console write the same list. Earliest first, both dates inclusive. Each period carries overlapping_bookings: how many of your not-cancelled bookings touch it -- so read_supply alone tells a key how many bookings fall in any window it closes; a count, never a booking.",
        "security": [
          {
            "credential": [
              "read_supply"
            ]
          }
        ],
        "x-parkena-scope": "read_supply",
        "x-parkena-rate-limits": [
          "credential.read"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/external_id"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlockedPeriodList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "422": {
            "$ref": "#/components/responses/Error422"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      },
      "put": {
        "operationId": "putBlockedPeriods",
        "summary": "Replace one car park's entire closure list",
        "description": "The list you send is the list that exists: closures typed into the console are part of the same list, and a push that omits them removes them. Periods overlapping sold bookings are not refused; each carries an overlapping_bookings count instead. With ?dry_run=true the write runs, is reported, and is rolled back.",
        "security": [
          {
            "credential": [
              "write_supply"
            ]
          }
        ],
        "x-parkena-scope": "write_supply",
        "x-parkena-rate-limits": [
          "credential.write.hourly",
          "credential.write"
        ],
        "x-parkena-dry-run-rate-limits": [
          "credential.write"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/external_id"
          },
          {
            "$ref": "#/components/parameters/dry_run"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BlockedPeriodsBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated or unchanged -- and EVERY dry run, whatever it reports.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlockedPeriodsWriteResult"
                }
              }
            }
          },
          "201": {
            "description": "Created. Never the answer to a dry run.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlockedPeriodsWriteResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "409": {
            "$ref": "#/components/responses/Error409"
          },
          "413": {
            "$ref": "#/components/responses/Error413"
          },
          "415": {
            "$ref": "#/components/responses/Error415"
          },
          "422": {
            "$ref": "#/components/responses/Error422"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    },
    "/bookings": {
      "get": {
        "operationId": "listBookings",
        "summary": "Your bookings, every channel, as a pull cursor",
        "description": "At-least-once: de-duplicate on reference, and save next only after you have durably stored the page. The cursor never points past the server's watermark (now minus five minutes), so recent rows can be delivered again -- that is correct.",
        "security": [
          {
            "credential": [
              "read_bookings"
            ]
          }
        ],
        "x-parkena-scope": "read_bookings",
        "x-parkena-rate-limits": [
          "credential.read"
        ],
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "The next token from your previous call; omit it (or send it empty) for \"from the beginning\". An opaque token this server issued -- never construct one.",
            "schema": {
              "type": "string",
              "maxLength": 512
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Asking for more than the maximum is refused (invalid_query), never reduced.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "422": {
            "$ref": "#/components/responses/Error422"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "503": {
            "$ref": "#/components/responses/Error503"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "credential": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "<key_id>.<secret>",
        "description": "Authorization: Bearer <key_id>.<secret> -- the two halves of one credential, issued by an owner or manager in the PARKENA console (Account -> API keys); the secret is shown once. The role names on each operation are the scope the credential must hold. No query-string fallback exists."
      }
    },
    "parameters": {
      "external_id": {
        "name": "external_id",
        "in": "path",
        "required": true,
        "description": "YOUR identifier for one of your car parks, opaque to PARKENA. Percent-encode a / or a space. An id none of your car parks has: the uniform 401 -- create the car park first with PUT /lots/{external_id}.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 200
        }
      },
      "external_id_upsert": {
        "name": "external_id",
        "in": "path",
        "required": true,
        "description": "YOUR identifier for the car park, opaque to PARKENA. Percent-encode a / or a space. An id none of your car parks has yet is not refused: the push CREATES a car park (201; 200 on a dry run, which keeps nothing). Ids are per operator, so another operator's id is new to you, and a mistyped id is a second car park rather than an error -- GET /lots (read_supply) and the console's API ids page list the ids PARKENA holds for you.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 200
        }
      },
      "dry_run": {
        "name": "dry_run",
        "in": "query",
        "required": false,
        "description": "The literal `true`, or absent. The write runs in full -- parse, validation, diff, the same statements and triggers inside the same transaction -- and is then rolled back; the answer is the real write's report plus \"dry_run\": true, and is 200 even where the real write would be 201. Any other value (false, 1, True, an empty value, the parameter twice) is 422 invalid_query naming dry_run. Spends the write burst bucket, not the hourly ceiling. Nothing in your estate changes; the credential is still recorded as used.",
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        }
      }
    },
    "headers": {
      "XRequestId": {
        "description": "Our id for this request, on every answer and every log line we wrote for it. Quote it to support.",
        "schema": {
          "type": "string"
        }
      },
      "RetryAfter": {
        "description": "Whole seconds.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "Allow": {
        "description": "The verbs this path serves, then OPTIONS.",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "Error401": {
        "description": "The uniform refusal: no or unparseable credential, unknown key, wrong secret, revoked, expired or suspended, missing scope, or -- on the routes under an existing car park, its rates and its closures -- an external_id that is not one of yours; indistinguishable by design. PUT /lots/{external_id} does not refuse an id you do not hold: it creates the car park. Use GET /ping to check a credential.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          },
          "WWW-Authenticate": {
            "schema": {
              "const": "Bearer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "error"
              ],
              "properties": {
                "error": {
                  "type": "string",
                  "enum": [
                    "unauthorized"
                  ]
                },
                "field": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Error404": {
        "description": "No such ROUTE. Never used for a resource.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "error"
              ],
              "properties": {
                "error": {
                  "type": "string",
                  "enum": [
                    "not_found"
                  ]
                },
                "field": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Error405": {
        "description": "Right path, wrong verb. The Allow header names the verbs the path serves.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          },
          "Allow": {
            "$ref": "#/components/headers/Allow"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "error"
              ],
              "properties": {
                "error": {
                  "type": "string",
                  "enum": [
                    "method_not_allowed"
                  ]
                },
                "field": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Error409": {
        "description": "A genuine race (two pushes created the same external_id at once). Retry.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "error"
              ],
              "properties": {
                "error": {
                  "type": "string",
                  "enum": [
                    "conflict"
                  ]
                },
                "field": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Error413": {
        "description": "Body over 64 KiB.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "error"
              ],
              "properties": {
                "error": {
                  "type": "string",
                  "enum": [
                    "payload_too_large"
                  ]
                },
                "field": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Error415": {
        "description": "Content-Type was not application/json.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "error"
              ],
              "properties": {
                "error": {
                  "type": "string",
                  "enum": [
                    "unsupported_media_type"
                  ]
                },
                "field": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Error422": {
        "description": "The request was understood and refused. `field`, when present, is your own field or parameter name.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "error"
              ],
              "properties": {
                "error": {
                  "type": "string",
                  "enum": [
                    "invalid_json",
                    "invalid_body",
                    "missing_field",
                    "unknown_field",
                    "field_belongs_to_another_route",
                    "invalid_query",
                    "invalid_cursor",
                    "too_many_lots",
                    "invalid_blocked_periods",
                    "invalid_timezone",
                    "invalid_coordinates",
                    "invalid_country_code",
                    "invalid_stay_bounds",
                    "incomplete_cancellation_policy",
                    "invalid_price_envelope",
                    "invalid_first_day_price",
                    "invalid_validity_window",
                    "currency_is_not_settlement_currency"
                  ]
                },
                "field": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Error429": {
        "description": "Over a rate limit. Honour Retry-After; being refused spends no token.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "error"
              ],
              "properties": {
                "error": {
                  "type": "string",
                  "enum": [
                    "rate_limited"
                  ]
                },
                "field": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Error500": {
        "description": "Our fault, logged in full against the X-Request-Id you were given. Quote it.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "error"
              ],
              "properties": {
                "error": {
                  "type": "string",
                  "enum": [
                    "internal_error"
                  ]
                },
                "field": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Error503": {
        "description": "A transient database condition. Retry after Retry-After; not a fault in your request.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "error"
              ],
              "properties": {
                "error": {
                  "type": "string",
                  "enum": [
                    "try_again"
                  ]
                },
                "field": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "unauthorized",
              "not_found",
              "method_not_allowed",
              "unsupported_media_type",
              "payload_too_large",
              "invalid_json",
              "invalid_body",
              "missing_field",
              "unknown_field",
              "field_belongs_to_another_route",
              "invalid_query",
              "invalid_cursor",
              "too_many_lots",
              "invalid_blocked_periods",
              "invalid_timezone",
              "invalid_coordinates",
              "invalid_country_code",
              "invalid_stay_bounds",
              "incomplete_cancellation_policy",
              "invalid_price_envelope",
              "invalid_first_day_price",
              "invalid_validity_window",
              "currency_is_not_settlement_currency",
              "conflict",
              "rate_limited",
              "try_again",
              "internal_error"
            ]
          },
          "field": {
            "type": "string",
            "description": "YOUR field or parameter name, echoed back. Never a database message, constraint or table name."
          }
        }
      },
      "Scope": {
        "type": "string",
        "enum": [
          "read_supply",
          "write_supply",
          "read_bookings"
        ]
      },
      "Ping": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "ok",
          "key_id",
          "scopes",
          "server_time"
        ],
        "properties": {
          "ok": {
            "const": true
          },
          "key_id": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Scope"
            }
          },
          "server_time": {
            "type": "string",
            "format": "date-time",
            "description": "Our clock, UTC. Informational only."
          }
        }
      },
      "LotBody": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "timezone",
          "handover",
          "online_bookable"
        ],
        "description": "One car park, whole. `rates` is refused here (field_belongs_to_another_route): prices are set at PUT /lots/{external_id}/rates. An array body is too_many_lots. x-parkena-reviewed marks the keys whose change voids a pending or approved listing review.",
        "properties": {
          "external_id": {
            "type": "string",
            "maxLength": 200,
            "description": "Optional. If present it must equal the one in the path: checked, not used."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "Blank counts as absent.",
            "x-parkena-reviewed": true
          },
          "timezone": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "An IANA zone name, e.g. Europe/Berlin (invalid_timezone otherwise). Every day-boundary calculation resolves through it.",
            "x-parkena-reviewed": true
          },
          "handover": {
            "type": "string",
            "enum": [
              "self",
              "attended"
            ],
            "x-parkena-reviewed": true
          },
          "online_bookable": {
            "type": "boolean",
            "description": "Your sales switch, and NOT reviewed content: turning it off takes the car park off sale without voiding anything.",
            "x-parkena-reviewed": false
          },
          "capacity": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 1000000,
            "description": "TOTAL spaces, never spaces free tonight. A car park takes no booking, and so cannot list, until this is above zero.",
            "x-parkena-reviewed": true
          },
          "latitude": {
            "type": [
              "string",
              "number",
              "null"
            ],
            "pattern": "^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)$",
            "description": "Degrees, -90..90, rounded to 6 decimal places. Sent with longitude or not at all (invalid_coordinates).",
            "x-parkena-reviewed": true
          },
          "longitude": {
            "type": [
              "string",
              "number",
              "null"
            ],
            "pattern": "^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)$",
            "description": "Degrees, -180..180, rounded to 6 decimal places. Sent with latitude or not at all (invalid_coordinates).",
            "x-parkena-reviewed": true
          },
          "city": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120,
            "x-parkena-reviewed": true
          },
          "country_code": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\s*[A-Za-z]{2}\\s*$",
            "description": "ISO 3166-1 alpha-2; stored upper-case.",
            "x-parkena-reviewed": true
          },
          "features": {
            "type": [
              "array",
              "null"
            ],
            "maxItems": 32,
            "items": {
              "type": "string",
              "enum": [
                "indoor",
                "security",
                "cameras",
                "gate_automation",
                "plate_recognition",
                "ev_charging",
                "disabled_access",
                "oversize_vehicle",
                "valet"
              ]
            },
            "description": "A set: sorted and de-duplicated before it is stored, so order never counts as a change.",
            "x-parkena-reviewed": true
          },
          "arrival": {
            "type": [
              "object",
              "null"
            ],
            "description": "Arrival guidance for the public listing, stored as you send it (at most 16 KiB). Recognised keys: entrance, access, gateWindow (any ONE of these three satisfies the listing requirement lot_arrival_guidance), streetAddress, parkingLevel, and transfer { line, first, last, frequencyMinutes, pickupPoint }. Prose members may be a string or an object keyed by language tag, e.g. { \"de\": \"...\", \"en\": \"...\" }.",
            "x-parkena-reviewed": true
          },
          "media": {
            "type": [
              "object",
              "null"
            ],
            "description": "Media references for the public listing (at most 16 KiB), stored as you send them unless one of them names lot-media outside your own folder (below). Recognised keys: heroUrl (an https:// address -- this is what satisfies the listing requirement lot_hero_image; an http:// picture is refused by it) and galleryUrls (an array of addresses). parkena.com displays only photos stored with PARKENA (uploaded in the console's Photos step): an address on any other host satisfies the requirement, but travellers see a placeholder instead of the picture. lot-media is the path of PARKENA's photo store, and a photo stored with PARKENA sits in your own operator's folder (`.../lot-media/<your operator>/...`, which the Photos step writes). So every string anywhere in media that contains lot-media -- read percent-decoded, with backslashes as slashes -- must have `/<your operator>/` directly after each lot-media in it, and no . or .. segment after the first; otherwise nothing is stored and the request is refused with 422 invalid_body, field media. That holds on any host: another operator's photo, and a path on your own website that happens to contain lot-media, are refused alike.",
            "x-parkena-reviewed": true
          },
          "min_advance_days": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 365,
            "default": 0,
            "description": "Absent or null stores 0.",
            "x-parkena-reviewed": true
          },
          "min_stay_days": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 365,
            "x-parkena-reviewed": true
          },
          "max_stay_days": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 365,
            "description": "All three day bounds stop at 365, how far ahead PARKENA quotes a stay at all.",
            "x-parkena-reviewed": true
          },
          "cancellation": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Cancellation"
              },
              {
                "type": "null"
              }
            ],
            "description": "Absent or null: no policy. All three keys or none (422 incomplete_cancellation_policy). Stored and shown to your listing reviewer, and REVIEWED content: changing it voids your listing review. It does not set the terms a PARKENA sale carries -- every PARKENA booking carries PARKENA's own cancellation terms, the same for every car park.",
            "x-parkena-reviewed": true
          }
        }
      },
      "Cancellation": {
        "type": "object",
        "additionalProperties": false,
        "dependentRequired": {
          "free_until_hours_before_check_in": [
            "penalty_percent_after",
            "no_show_forfeits_full"
          ],
          "penalty_percent_after": [
            "free_until_hours_before_check_in",
            "no_show_forfeits_full"
          ],
          "no_show_forfeits_full": [
            "free_until_hours_before_check_in",
            "penalty_percent_after"
          ]
        },
        "description": "All three keys or none (422 incomplete_cancellation_policy). Stored and shown to your listing reviewer, and REVIEWED content: changing it voids your listing review. It does not set the terms a PARKENA sale carries -- every PARKENA booking carries PARKENA's own cancellation terms, the same for every car park.",
        "properties": {
          "free_until_hours_before_check_in": {
            "type": "integer",
            "minimum": 0,
            "maximum": 8760
          },
          "penalty_percent_after": {
            "type": [
              "string",
              "number"
            ],
            "pattern": "^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)$",
            "description": "0..100, two decimal places."
          },
          "no_show_forfeits_full": {
            "type": "boolean"
          }
        }
      },
      "Lot": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "external_id",
          "name",
          "timezone",
          "handover",
          "online_bookable",
          "capacity",
          "latitude",
          "longitude",
          "city",
          "country_code",
          "features",
          "arrival",
          "media",
          "min_advance_days",
          "min_stay_days",
          "max_stay_days",
          "cancellation"
        ],
        "properties": {
          "external_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "timezone": {
            "type": "string"
          },
          "handover": {
            "type": "string",
            "enum": [
              "self",
              "attended"
            ]
          },
          "online_bookable": {
            "type": "boolean"
          },
          "capacity": {
            "type": [
              "integer",
              "null"
            ]
          },
          "latitude": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^-?\\d+(\\.\\d+)?$"
          },
          "longitude": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^-?\\d+(\\.\\d+)?$"
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "country_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "features": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "indoor",
                "security",
                "cameras",
                "gate_automation",
                "plate_recognition",
                "ev_charging",
                "disabled_access",
                "oversize_vehicle",
                "valet"
              ]
            }
          },
          "arrival": {
            "type": [
              "object",
              "null"
            ],
            "description": "Arrival guidance for the public listing, stored as you send it (at most 16 KiB). Recognised keys: entrance, access, gateWindow (any ONE of these three satisfies the listing requirement lot_arrival_guidance), streetAddress, parkingLevel, and transfer { line, first, last, frequencyMinutes, pickupPoint }. Prose members may be a string or an object keyed by language tag, e.g. { \"de\": \"...\", \"en\": \"...\" }."
          },
          "media": {
            "type": [
              "object",
              "null"
            ],
            "description": "Media references for the public listing (at most 16 KiB), stored as you send them unless one of them names lot-media outside your own folder (below). Recognised keys: heroUrl (an https:// address -- this is what satisfies the listing requirement lot_hero_image; an http:// picture is refused by it) and galleryUrls (an array of addresses). parkena.com displays only photos stored with PARKENA (uploaded in the console's Photos step): an address on any other host satisfies the requirement, but travellers see a placeholder instead of the picture. lot-media is the path of PARKENA's photo store, and a photo stored with PARKENA sits in your own operator's folder (`.../lot-media/<your operator>/...`, which the Photos step writes). So every string anywhere in media that contains lot-media -- read percent-decoded, with backslashes as slashes -- must have `/<your operator>/` directly after each lot-media in it, and no . or .. segment after the first; otherwise nothing is stored and the request is refused with 422 invalid_body, field media. That holds on any host: another operator's photo, and a path on your own website that happens to contain lot-media, are refused alike."
          },
          "min_advance_days": {
            "type": "integer"
          },
          "min_stay_days": {
            "type": [
              "integer",
              "null"
            ]
          },
          "max_stay_days": {
            "type": [
              "integer",
              "null"
            ]
          },
          "cancellation": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": false,
            "required": [
              "free_until_hours_before_check_in",
              "penalty_percent_after",
              "no_show_forfeits_full"
            ],
            "properties": {
              "free_until_hours_before_check_in": {
                "type": "integer"
              },
              "penalty_percent_after": {
                "type": "string",
                "pattern": "^-?\\d+(\\.\\d+)?$"
              },
              "no_show_forfeits_full": {
                "type": "boolean"
              }
            },
            "description": "All three keys or none (422 incomplete_cancellation_policy). Stored and shown to your listing reviewer, and REVIEWED content: changing it voids your listing review. It does not set the terms a PARKENA sale carries -- every PARKENA booking carries PARKENA's own cancellation terms, the same for every car park."
          }
        }
      },
      "LotListEntry": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "external_id",
          "name",
          "timezone",
          "handover",
          "online_bookable",
          "capacity",
          "latitude",
          "longitude",
          "city",
          "country_code",
          "features",
          "arrival",
          "media",
          "min_advance_days",
          "min_stay_days",
          "max_stay_days",
          "cancellation",
          "rates"
        ],
        "properties": {
          "external_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "timezone": {
            "type": "string"
          },
          "handover": {
            "type": "string",
            "enum": [
              "self",
              "attended"
            ]
          },
          "online_bookable": {
            "type": "boolean"
          },
          "capacity": {
            "type": [
              "integer",
              "null"
            ]
          },
          "latitude": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^-?\\d+(\\.\\d+)?$"
          },
          "longitude": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^-?\\d+(\\.\\d+)?$"
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "country_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "features": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "indoor",
                "security",
                "cameras",
                "gate_automation",
                "plate_recognition",
                "ev_charging",
                "disabled_access",
                "oversize_vehicle",
                "valet"
              ]
            }
          },
          "arrival": {
            "type": [
              "object",
              "null"
            ],
            "description": "Arrival guidance for the public listing, stored as you send it (at most 16 KiB). Recognised keys: entrance, access, gateWindow (any ONE of these three satisfies the listing requirement lot_arrival_guidance), streetAddress, parkingLevel, and transfer { line, first, last, frequencyMinutes, pickupPoint }. Prose members may be a string or an object keyed by language tag, e.g. { \"de\": \"...\", \"en\": \"...\" }."
          },
          "media": {
            "type": [
              "object",
              "null"
            ],
            "description": "Media references for the public listing (at most 16 KiB), stored as you send them unless one of them names lot-media outside your own folder (below). Recognised keys: heroUrl (an https:// address -- this is what satisfies the listing requirement lot_hero_image; an http:// picture is refused by it) and galleryUrls (an array of addresses). parkena.com displays only photos stored with PARKENA (uploaded in the console's Photos step): an address on any other host satisfies the requirement, but travellers see a placeholder instead of the picture. lot-media is the path of PARKENA's photo store, and a photo stored with PARKENA sits in your own operator's folder (`.../lot-media/<your operator>/...`, which the Photos step writes). So every string anywhere in media that contains lot-media -- read percent-decoded, with backslashes as slashes -- must have `/<your operator>/` directly after each lot-media in it, and no . or .. segment after the first; otherwise nothing is stored and the request is refused with 422 invalid_body, field media. That holds on any host: another operator's photo, and a path on your own website that happens to contain lot-media, are refused alike."
          },
          "min_advance_days": {
            "type": "integer"
          },
          "min_stay_days": {
            "type": [
              "integer",
              "null"
            ]
          },
          "max_stay_days": {
            "type": [
              "integer",
              "null"
            ]
          },
          "cancellation": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": false,
            "required": [
              "free_until_hours_before_check_in",
              "penalty_percent_after",
              "no_show_forfeits_full"
            ],
            "properties": {
              "free_until_hours_before_check_in": {
                "type": "integer"
              },
              "penalty_percent_after": {
                "type": "string",
                "pattern": "^-?\\d+(\\.\\d+)?$"
              },
              "no_show_forfeits_full": {
                "type": "boolean"
              }
            },
            "description": "All three keys or none (422 incomplete_cancellation_policy). Stored and shown to your listing reviewer, and REVIEWED content: changing it voids your listing review. It does not set the terms a PARKENA sale carries -- every PARKENA booking carries PARKENA's own cancellation terms, the same for every car park."
          },
          "rates": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Rates"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "LotPage": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "lots",
          "next_after"
        ],
        "properties": {
          "lots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LotListEntry"
            }
          },
          "next_after": {
            "type": [
              "string",
              "null"
            ],
            "description": "A string only when the page was full; null otherwise."
          }
        }
      },
      "LotWriteResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "external_id",
          "result",
          "changed",
          "listing_review_superseded",
          "lot"
        ],
        "properties": {
          "external_id": {
            "type": "string"
          },
          "result": {
            "type": "string",
            "enum": [
              "created",
              "updated",
              "unchanged"
            ],
            "description": "\"unchanged\" means no statement was issued at all -- no lock, no write, no trigger. On a dry run, what the real write would have done."
          },
          "changed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The field names that actually differ. Empty on \"unchanged\"."
          },
          "listing_review_superseded": {
            "type": "boolean",
            "description": "True if this write voided (or, on a dry run, would void) your PARKENA listing review."
          },
          "lot": {
            "$ref": "#/components/schemas/Lot"
          },
          "dry_run": {
            "const": true,
            "description": "Present, and true, only on a ?dry_run=true request: everything above is what the write did inside a transaction that was then rolled back."
          }
        }
      },
      "RatesBody": {
        "type": "object",
        "additionalProperties": false,
        "oneOf": [
          {
            "required": [
              "fixed_price"
            ],
            "not": {
              "anyOf": [
                {
                  "required": [
                    "base_price"
                  ]
                },
                {
                  "required": [
                    "floor_price"
                  ]
                },
                {
                  "required": [
                    "ceiling_price"
                  ]
                }
              ]
            }
          },
          {
            "required": [
              "base_price",
              "floor_price",
              "ceiling_price"
            ],
            "not": {
              "required": [
                "fixed_price"
              ]
            }
          }
        ],
        "description": "A range (floor_price, base_price, ceiling_price) or one fixed_price, never both. Money is refused rather than rounded: two decimal places; a third non-zero digit is invalid_body.",
        "properties": {
          "currency": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\s*[A-Za-z]{3}\\s*$",
            "description": "ISO 4217. Defaults to your settlement currency and cannot be anything else (currency_is_not_settlement_currency)."
          },
          "fixed_price": {
            "type": [
              "string",
              "number"
            ],
            "pattern": "^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)$",
            "description": "One price: floor = base = ceiling. Cannot be combined with the range fields."
          },
          "base_price": {
            "type": [
              "string",
              "number"
            ],
            "pattern": "^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)$",
            "description": "Between floor and ceiling."
          },
          "floor_price": {
            "type": [
              "string",
              "number"
            ],
            "pattern": "^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)$",
            "description": "At most base_price (invalid_price_envelope)."
          },
          "ceiling_price": {
            "type": [
              "string",
              "number"
            ],
            "pattern": "^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)$",
            "description": "At least base_price (invalid_price_envelope)."
          },
          "min_first_day_price": {
            "type": [
              "string",
              "number",
              "null"
            ],
            "pattern": "^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)$",
            "description": "Inside the envelope (invalid_first_day_price)."
          },
          "dynamic": {
            "type": [
              "boolean",
              "null"
            ],
            "default": false
          },
          "valid_from": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          },
          "valid_to": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Not before valid_from (invalid_validity_window)."
          }
        }
      },
      "Rates": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "channel",
          "currency",
          "base_price",
          "floor_price",
          "ceiling_price",
          "min_first_day_price",
          "dynamic",
          "valid_from",
          "valid_to"
        ],
        "properties": {
          "channel": {
            "const": "parkena"
          },
          "currency": {
            "type": "string"
          },
          "base_price": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$"
          },
          "floor_price": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$"
          },
          "ceiling_price": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$"
          },
          "min_first_day_price": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^-?\\d+(\\.\\d+)?$"
          },
          "dynamic": {
            "type": "boolean"
          },
          "valid_from": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          },
          "valid_to": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          }
        }
      },
      "RatesWriteResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "external_id",
          "result",
          "changed",
          "envelope_widened",
          "rates"
        ],
        "properties": {
          "external_id": {
            "type": "string"
          },
          "result": {
            "type": "string",
            "enum": [
              "created",
              "updated",
              "unchanged"
            ],
            "description": "\"unchanged\" means no statement was issued at all -- no lock, no write, no trigger. On a dry run, what the real write would have done."
          },
          "changed": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "envelope_widened": {
            "type": "boolean",
            "description": "True when the envelope moved outward (floor down, ceiling up, currency changed, dynamic flipped) or did not exist before. Conservative: it can be true where nothing was voided, never false where something was."
          },
          "rates": {
            "$ref": "#/components/schemas/Rates"
          },
          "dry_run": {
            "const": true,
            "description": "Present, and true, only on a ?dry_run=true request: everything above is what the write did inside a transaction that was then rolled back."
          }
        }
      },
      "BlockedPeriodInput": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "starts_on",
          "ends_on"
        ],
        "properties": {
          "starts_on": {
            "type": "string",
            "format": "date",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "The first blocked day, inclusive."
          },
          "ends_on": {
            "type": "string",
            "format": "date",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "The LAST blocked day, inclusive -- not the day after."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200,
            "description": "Your label. Blank is refused; stored verbatim, never trimmed."
          }
        }
      },
      "BlockedPeriodsBody": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "blocked_periods"
        ],
        "properties": {
          "blocked_periods": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/BlockedPeriodInput"
            },
            "description": "Every date a real calendar day in 2020-01-01..2032-12-31; no two entries may overlap, inclusively. Every content refusal is 422 invalid_blocked_periods with field naming the entry, e.g. blocked_periods[3].ends_on."
          }
        }
      },
      "BlockedPeriod": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "starts_on",
          "ends_on",
          "reason",
          "overlapping_bookings"
        ],
        "properties": {
          "starts_on": {
            "type": "string",
            "format": "date",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          },
          "ends_on": {
            "type": "string",
            "format": "date",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "overlapping_bookings": {
            "type": "integer",
            "minimum": 0,
            "description": "How many of this car park's bookings -- every channel, every status but cancelled, completed stays included -- touch the period. A count, never a booking."
          }
        }
      },
      "BlockedPeriodList": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "external_id",
          "blocked_periods"
        ],
        "properties": {
          "external_id": {
            "type": "string"
          },
          "blocked_periods": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BlockedPeriod"
            }
          }
        }
      },
      "BlockedPeriodsWriteResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "external_id",
          "result",
          "added",
          "removed",
          "blocked_periods"
        ],
        "properties": {
          "external_id": {
            "type": "string"
          },
          "result": {
            "type": "string",
            "enum": [
              "created",
              "updated",
              "unchanged"
            ],
            "description": "\"unchanged\" means no statement was issued at all -- no lock, no write, no trigger. On a dry run, what the real write would have done."
          },
          "added": {
            "type": "integer",
            "minimum": 0
          },
          "removed": {
            "type": "integer",
            "minimum": 0
          },
          "blocked_periods": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BlockedPeriod"
            },
            "description": "The stored list, read back after the write, with fresh counts."
          },
          "dry_run": {
            "const": true,
            "description": "Present, and true, only on a ?dry_run=true request: everything above is what the write did inside a transaction that was then rolled back."
          }
        }
      },
      "Booking": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "reference",
          "external_reference",
          "lot_external_id",
          "lot_name",
          "timezone",
          "check_in_local",
          "check_out_local",
          "parking_days",
          "status",
          "payment_status",
          "currency",
          "total",
          "channel",
          "flight_number",
          "customer",
          "vehicle_plates",
          "created_at",
          "cancelled_at"
        ],
        "properties": {
          "reference": {
            "type": "string",
            "description": "PARKENA's booking reference: THE de-duplication key."
          },
          "external_reference": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set only from the console; echoed back, never keyed on."
          },
          "lot_external_id": {
            "type": "string"
          },
          "lot_name": {
            "type": "string"
          },
          "timezone": {
            "type": "string",
            "description": "The car park's IANA zone; read the two local times in it."
          },
          "check_in_local": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
            "description": "The car park's own wall clock, with NO zone offset."
          },
          "check_out_local": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$"
          },
          "parking_days": {
            "type": [
              "integer",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "confirmed",
              "checked_in",
              "completed",
              "cancelled"
            ]
          },
          "payment_status": {
            "type": "string",
            "enum": [
              "pending",
              "paid",
              "partial",
              "refunded",
              "expired",
              "not_required"
            ]
          },
          "currency": {
            "type": "string"
          },
          "total": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "description": "A decimal string, never a float."
          },
          "channel": {
            "type": "string",
            "description": "\"parkena\" for bookings PARKENA sold; the console's channel (e.g. \"direct\") otherwise. Filter on it if your own system is the source of truth for direct sales."
          },
          "flight_number": {
            "type": [
              "string",
              "null"
            ]
          },
          "customer": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "first_name",
              "last_name",
              "email",
              "phone"
            ],
            "properties": {
              "first_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "last_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "email": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "phone": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "vehicle_plates": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "cancelled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "BookingPage": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "bookings",
          "next",
          "has_more"
        ],
        "properties": {
          "bookings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Booking"
            }
          },
          "next": {
            "type": "string",
            "description": "Pass as since on your next call."
          },
          "has_more": {
            "type": "boolean"
          }
        }
      }
    }
  },
  "x-parkena-limits": {
    "max_body_bytes": 65536,
    "lots_per_request": 1,
    "blocked_periods_per_request": 100
  },
  "x-parkena-rate-limits": [
    {
      "policy": "ip",
      "bucket": "All requests",
      "counted_against": "source address",
      "burst": 240,
      "refill_per_second": 4,
      "refill": "4 / second",
      "held_in": "edge-worker-memory"
    },
    {
      "policy": "ip.failure",
      "bucket": "Failed authentications",
      "counted_against": "source address",
      "burst": 20,
      "refill_per_second": 0.3333333333333333,
      "refill": "1 per 3 seconds",
      "held_in": "edge-worker-memory"
    },
    {
      "policy": "credential.read",
      "bucket": "Reads",
      "counted_against": "credential",
      "burst": 120,
      "refill_per_second": 2,
      "refill": "2 / second",
      "held_in": "edge-worker-memory"
    },
    {
      "policy": "credential.write",
      "bucket": "Writes (burst)",
      "counted_against": "credential",
      "burst": 60,
      "refill_per_second": 1,
      "refill": "1 / second",
      "held_in": "edge-worker-memory"
    },
    {
      "policy": "credential.write.hourly",
      "bucket": "Writes (hourly)",
      "counted_against": "credential",
      "burst": 1000,
      "refill_per_second": 0.2777777777777778,
      "refill": "1000 / hour",
      "held_in": "edge-worker-memory"
    }
  ]
}
