{
  "openapi": "3.0.3",
  "info": {
    "title": "LASERWAR CRM Calendar Availability API",
    "version": "1.0.0",
    "description": "Account-scoped server-to-server availability and prepayment checkout. Enable in Settings > Calendar API. GET/check is advisory. Creating an intent atomically holds capacity. An issued bank payment keeps capacity until an authenticated bank status confirms payment/cancellation; ordinary hold TTL applies only before payment starts. Instructor shifts and unassigned equipment are not guaranteed."
  },
  "servers": [
    {
      "url": "https://{club}.laserwar.club/api/v1",
      "variables": {
        "club": {
          "default": "lasertag-test",
          "description": "The club subdomain matching the integration key."
        }
      }
    }
  ],
  "security": [
    {
      "CalendarKey": []
    }
  ],
  "paths": {
    "/availability": {
      "get": {
        "operationId": "listAvailability",
        "summary": "Read available dates and intervals",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "description": "First date, in the club time zone.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Last date inclusive; defaults to from. Maximum 31 days and 365 days ahead.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "venue_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 2147483647,
              "default": 0,
              "description": "Active club venue; 0 selects any eligible venue."
            }
          },
          {
            "name": "duration_minutes",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 5,
              "maximum": 1440,
              "default": 60
            }
          },
          {
            "name": "players",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000,
              "default": 1
            }
          },
          {
            "name": "game_type_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 2147483647,
              "default": 0,
              "description": "Active club game type; metadata, not a venue filter."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Availability"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CalendarError"
          },
          "401": {
            "$ref": "#/components/responses/CalendarError"
          },
          "403": {
            "$ref": "#/components/responses/CalendarError"
          },
          "404": {
            "$ref": "#/components/responses/CalendarError"
          },
          "405": {
            "$ref": "#/components/responses/CalendarError"
          },
          "409": {
            "$ref": "#/components/responses/CalendarError"
          },
          "413": {
            "$ref": "#/components/responses/CalendarError"
          },
          "422": {
            "$ref": "#/components/responses/CalendarError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          },
          "502": {
            "$ref": "#/components/responses/CalendarError"
          },
          "503": {
            "$ref": "#/components/responses/CalendarError"
          }
        }
      }
    },
    "/availability/venues": {
      "get": {
        "operationId": "listVenues",
        "summary": "Read active venues and configured capacity",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "timezone",
                        "venues"
                      ],
                      "properties": {
                        "timezone": {
                          "type": "string"
                        },
                        "venues": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Venue"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CalendarError"
          },
          "401": {
            "$ref": "#/components/responses/CalendarError"
          },
          "403": {
            "$ref": "#/components/responses/CalendarError"
          },
          "404": {
            "$ref": "#/components/responses/CalendarError"
          },
          "405": {
            "$ref": "#/components/responses/CalendarError"
          },
          "409": {
            "$ref": "#/components/responses/CalendarError"
          },
          "413": {
            "$ref": "#/components/responses/CalendarError"
          },
          "422": {
            "$ref": "#/components/responses/CalendarError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          },
          "502": {
            "$ref": "#/components/responses/CalendarError"
          },
          "503": {
            "$ref": "#/components/responses/CalendarError"
          }
        }
      }
    },
    "/availability/check": {
      "post": {
        "operationId": "checkAvailability",
        "summary": "Check an interval without holding it",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "available",
                        "timezone"
                      ],
                      "properties": {
                        "available": {
                          "type": "boolean"
                        },
                        "timezone": {
                          "type": "string"
                        },
                        "reason": {
                          "type": "string",
                          "enum": [
                            "outside_working_time",
                            "no_capacity"
                          ]
                        },
                        "slot": {
                          "$ref": "#/components/schemas/Slot"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CalendarError"
          },
          "401": {
            "$ref": "#/components/responses/CalendarError"
          },
          "403": {
            "$ref": "#/components/responses/CalendarError"
          },
          "404": {
            "$ref": "#/components/responses/CalendarError"
          },
          "405": {
            "$ref": "#/components/responses/CalendarError"
          },
          "409": {
            "$ref": "#/components/responses/CalendarError"
          },
          "413": {
            "$ref": "#/components/responses/CalendarError"
          },
          "422": {
            "$ref": "#/components/responses/CalendarError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          },
          "502": {
            "$ref": "#/components/responses/CalendarError"
          },
          "503": {
            "$ref": "#/components/responses/CalendarError"
          }
        }
      }
    },
    "/prepayment-intents": {
      "post": {
        "operationId": "createPrepaymentIntent",
        "summary": "Atomically recheck availability and hold one event place",
        "description": "Requires a key with prepayment permission. Amount is calculated and authorized by the caller's trusted backend. Same key + same request returns the original intent, even if expired; a different request with the same key returns 409.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 128,
              "pattern": "^[A-Za-z0-9_.:-]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IntentInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Intent"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CalendarError"
          },
          "401": {
            "$ref": "#/components/responses/CalendarError"
          },
          "403": {
            "$ref": "#/components/responses/CalendarError"
          },
          "404": {
            "$ref": "#/components/responses/CalendarError"
          },
          "405": {
            "$ref": "#/components/responses/CalendarError"
          },
          "409": {
            "$ref": "#/components/responses/CalendarError"
          },
          "413": {
            "$ref": "#/components/responses/CalendarError"
          },
          "422": {
            "$ref": "#/components/responses/CalendarError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          },
          "502": {
            "$ref": "#/components/responses/CalendarError"
          },
          "503": {
            "$ref": "#/components/responses/CalendarError"
          }
        }
      }
    },
    "/prepayment-intents/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^[a-f0-9]{32}$"
          },
          "description": "Public intent ID created by this same integration key."
        }
      ],
      "get": {
        "operationId": "getPrepaymentIntent",
        "summary": "Read own intent and confirmed payment state",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Intent"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CalendarError"
          },
          "401": {
            "$ref": "#/components/responses/CalendarError"
          },
          "403": {
            "$ref": "#/components/responses/CalendarError"
          },
          "404": {
            "$ref": "#/components/responses/CalendarError"
          },
          "405": {
            "$ref": "#/components/responses/CalendarError"
          },
          "409": {
            "$ref": "#/components/responses/CalendarError"
          },
          "413": {
            "$ref": "#/components/responses/CalendarError"
          },
          "422": {
            "$ref": "#/components/responses/CalendarError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          },
          "502": {
            "$ref": "#/components/responses/CalendarError"
          },
          "503": {
            "$ref": "#/components/responses/CalendarError"
          }
        }
      },
      "delete": {
        "operationId": "cancelPrepaymentIntent",
        "summary": "Cancel a hold before payment initialization",
        "description": "Returns 409 once payment starts; first cancel at the bank and use sync. Confirmed prepayments use the normal CRM refund/cancellation workflow.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Intent"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CalendarError"
          },
          "401": {
            "$ref": "#/components/responses/CalendarError"
          },
          "403": {
            "$ref": "#/components/responses/CalendarError"
          },
          "404": {
            "$ref": "#/components/responses/CalendarError"
          },
          "405": {
            "$ref": "#/components/responses/CalendarError"
          },
          "409": {
            "$ref": "#/components/responses/CalendarError"
          },
          "413": {
            "$ref": "#/components/responses/CalendarError"
          },
          "422": {
            "$ref": "#/components/responses/CalendarError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          },
          "502": {
            "$ref": "#/components/responses/CalendarError"
          },
          "503": {
            "$ref": "#/components/responses/CalendarError"
          }
        }
      }
    },
    "/prepayment-intents/{id}/payment": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^[a-f0-9]{32}$"
          },
          "description": "Public intent ID created by this same integration key."
        }
      ],
      "post": {
        "operationId": "initializePrepayment",
        "summary": "Recheck the held interval and get a bank payment URL",
        "description": "Uses the club default bank. No gateway credentials or amounts are supplied here. Repeats reuse an existing URL. Concurrent initialization returns 409. A timeout returns 502 and retains capacity in payment_review. This request can take up to the bank gateway timeout.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Intent"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CalendarError"
          },
          "401": {
            "$ref": "#/components/responses/CalendarError"
          },
          "403": {
            "$ref": "#/components/responses/CalendarError"
          },
          "404": {
            "$ref": "#/components/responses/CalendarError"
          },
          "405": {
            "$ref": "#/components/responses/CalendarError"
          },
          "409": {
            "$ref": "#/components/responses/CalendarError"
          },
          "413": {
            "$ref": "#/components/responses/CalendarError"
          },
          "422": {
            "$ref": "#/components/responses/CalendarError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          },
          "502": {
            "$ref": "#/components/responses/CalendarError"
          },
          "503": {
            "$ref": "#/components/responses/CalendarError"
          }
        }
      }
    },
    "/prepayment-intents/{id}/sync": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^[a-f0-9]{32}$"
          },
          "description": "Public intent ID created by this same integration key."
        }
      ],
      "post": {
        "operationId": "syncPrepayment",
        "summary": "Refresh status directly from the payment provider",
        "description": "The integration cannot assert successful payment. CRM queries the provider with its saved bank credentials. Provider unavailability preserves capacity. No provider identifier requires operator review.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Intent"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CalendarError"
          },
          "401": {
            "$ref": "#/components/responses/CalendarError"
          },
          "403": {
            "$ref": "#/components/responses/CalendarError"
          },
          "404": {
            "$ref": "#/components/responses/CalendarError"
          },
          "405": {
            "$ref": "#/components/responses/CalendarError"
          },
          "409": {
            "$ref": "#/components/responses/CalendarError"
          },
          "413": {
            "$ref": "#/components/responses/CalendarError"
          },
          "422": {
            "$ref": "#/components/responses/CalendarError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          },
          "502": {
            "$ref": "#/components/responses/CalendarError"
          },
          "503": {
            "$ref": "#/components/responses/CalendarError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "CalendarKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "Opaque integration token",
        "description": "lwcal_ followed by 64 hex characters. Never embed in mobile binaries or public JavaScript. Limit: 120 requests/minute/key."
      }
    },
    "schemas": {
      "CheckInput": {
        "type": "object",
        "required": [
          "date",
          "time"
        ],
        "properties": {
          "date": {
            "type": "string",
            "format": "date"
          },
          "time": {
            "type": "string",
            "pattern": "^(?:[01][0-9]|2[0-3]):[0-5][0-9]$"
          },
          "venue_id": {
            "type": "integer",
            "minimum": 0,
            "maximum": 2147483647,
            "default": 0,
            "description": "Active club venue; 0 selects any eligible venue."
          },
          "duration_minutes": {
            "type": "integer",
            "minimum": 5,
            "maximum": 1440,
            "default": 60
          },
          "players": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10000,
            "default": 1
          },
          "game_type_id": {
            "type": "integer",
            "minimum": 0,
            "maximum": 2147483647,
            "default": 0,
            "description": "Active club game type; metadata, not a venue filter."
          }
        }
      },
      "IntentInput": {
        "type": "object",
        "required": [
          "date",
          "time",
          "amount_rubles",
          "customer"
        ],
        "properties": {
          "date": {
            "type": "string",
            "format": "date"
          },
          "time": {
            "type": "string",
            "pattern": "^(?:[01][0-9]|2[0-3]):[0-5][0-9]$"
          },
          "venue_id": {
            "type": "integer",
            "minimum": 0,
            "maximum": 2147483647,
            "default": 0,
            "description": "Active club venue; 0 selects any eligible venue."
          },
          "duration_minutes": {
            "type": "integer",
            "minimum": 5,
            "maximum": 1440,
            "default": 60
          },
          "players": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10000,
            "default": 1
          },
          "game_type_id": {
            "type": "integer",
            "minimum": 0,
            "maximum": 2147483647,
            "default": 0,
            "description": "Active club game type; metadata, not a venue filter."
          },
          "amount_rubles": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10000000
          },
          "total_rubles": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10000000,
            "description": "Full event price, at least amount_rubles; defaults to amount_rubles."
          },
          "customer": {
            "type": "object",
            "required": [
              "name",
              "phone"
            ],
            "properties": {
              "name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 200
              },
              "phone": {
                "type": "string",
                "minLength": 7,
                "maxLength": 25
              },
              "email": {
                "type": "string",
                "maxLength": 254,
                "description": "Optional valid email, or empty; existing bank-email fallback applies."
              }
            }
          },
          "comment": {
            "type": "string",
            "maxLength": 2000
          }
        }
      },
      "Slot": {
        "type": "object",
        "required": [
          "venue_id",
          "date",
          "time",
          "start",
          "end",
          "starts_at",
          "ends_at",
          "remaining_capacity",
          "player_limit"
        ],
        "properties": {
          "venue_id": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2147483647
          },
          "date": {
            "type": "string",
            "format": "date"
          },
          "time": {
            "type": "string"
          },
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          },
          "starts_at": {
            "type": "integer",
            "format": "int64"
          },
          "ends_at": {
            "type": "integer",
            "format": "int64"
          },
          "remaining_capacity": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2,
            "description": "Remaining simultaneous event places, not player count."
          },
          "player_limit": {
            "type": "integer",
            "nullable": true,
            "description": "Configured maximum guests per party, null when unspecified."
          }
        }
      },
      "Venue": {
        "type": "object",
        "required": [
          "id",
          "name_ru",
          "name_en",
          "parallel_capacity",
          "player_limit"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2147483647
          },
          "name_ru": {
            "type": "string"
          },
          "name_en": {
            "type": "string"
          },
          "parallel_capacity": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2
          },
          "player_limit": {
            "type": "integer",
            "nullable": true
          }
        }
      },
      "Availability": {
        "type": "object",
        "required": [
          "timezone",
          "checked_at",
          "duration_minutes",
          "step_minutes",
          "days",
          "slots",
          "advisory",
          "constraints"
        ],
        "properties": {
          "timezone": {
            "type": "string",
            "description": "IANA time zone, such as Europe/Moscow."
          },
          "checked_at": {
            "type": "string",
            "format": "date-time"
          },
          "duration_minutes": {
            "type": "integer",
            "minimum": 5,
            "maximum": 1440
          },
          "step_minutes": {
            "type": "integer",
            "minimum": 1,
            "maximum": 180
          },
          "days": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "date",
                "available",
                "slot_count"
              ],
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date"
                },
                "available": {
                  "type": "boolean"
                },
                "slot_count": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 2147483647
                }
              }
            }
          },
          "slots": {
            "type": "array",
            "maxItems": 10000,
            "items": {
              "$ref": "#/components/schemas/Slot"
            }
          },
          "advisory": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "constraints": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Intent": {
        "type": "object",
        "required": [
          "id",
          "state",
          "venue_id",
          "start",
          "end",
          "timezone",
          "expires_at",
          "amount_rubles",
          "order_id"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-f0-9]{32}$"
          },
          "state": {
            "type": "string",
            "enum": [
              "held",
              "expired",
              "payment_pending",
              "payment_review",
              "confirmed",
              "cancelled"
            ]
          },
          "venue_id": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2147483647
          },
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          },
          "timezone": {
            "type": "string"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Expiry before payment starts. Does NOT release an issued bank payment."
          },
          "amount_rubles": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10000000
          },
          "order_id": {
            "type": "integer",
            "nullable": true
          },
          "payment_url": {
            "type": "string",
            "format": "uri",
            "description": "Present only for an initialized pending payment. Redirect the guest here."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "details"
            ],
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "details": {
                "oneOf": [
                  {
                    "type": "object",
                    "additionalProperties": true
                  },
                  {
                    "type": "array",
                    "items": {}
                  }
                ]
              }
            }
          }
        }
      }
    },
    "responses": {
      "CalendarError": {
        "description": "Request rejected; use error.code for client messages.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimit": {
        "description": "Request rejected; use error.code for client messages.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds before retry; 60."
          }
        }
      }
    }
  }
}
