{
  "openapi": "3.1.0",
  "info": {
    "title": "Wodloop owner API",
    "version": "1.0.0",
    "description": "A box's figures, read only, for a spreadsheet, a BI tool or a dashboard's server. The owner makes a key in the back-office (Settings, API) and chooses which facilities it reads. Every figure is the back-office's own, one row per facility. Money is in cents with its currency; `from` and `to` are dates in the facility's time zone, both included. Changes within v1 only add.",
    "contact": {
      "email": "support@wodloop.com"
    }
  },
  "servers": [
    {
      "url": "https://www.wodloop.com/api/v1"
    }
  ],
  "security": [
    {
      "bearer": []
    },
    {
      "api_key": []
    }
  ],
  "tags": [
    {
      "name": "Facilities"
    },
    {
      "name": "Figures"
    },
    {
      "name": "Meta"
    }
  ],
  "paths": {
    "/places": {
      "get": {
        "operationId": "listPlaces",
        "tags": [
          "Facilities"
        ],
        "summary": "The facilities this key reads",
        "description": "Every facility the key reads, with its time zone and currency. A closed facility is left out.",
        "parameters": [
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "The facilities.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Place"
                      }
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "place_id,...\r\n12,...\r\n"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/places/{id}": {
      "get": {
        "operationId": "getPlace",
        "tags": [
          "Facilities"
        ],
        "summary": "One facility",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "The facility.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Place"
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stats/summary": {
      "get": {
        "operationId": "statsSummary",
        "tags": [
          "Figures"
        ],
        "summary": "Headline figures, one row per facility",
        "description": "Members and monthly recurring revenue as of now; over the period, bookings, distinct attendees, the schedule's seats and fill, and the money in, out and net. The figures of the back-office Dashboard and Overview.",
        "parameters": [
          {
            "$ref": "#/components/parameters/place_id"
          },
          {
            "$ref": "#/components/parameters/from"
          },
          {
            "$ref": "#/components/parameters/to"
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "One or more rows per facility, each with its place_id.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Summary"
                      }
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "place_id,...\r\n12,...\r\n"
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/InvalidParameter"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stats/revenue": {
      "get": {
        "operationId": "statsRevenue",
        "tags": [
          "Figures"
        ],
        "summary": "Money per month",
        "description": "One row per facility and per month the period touches (the first and last months count only their days in the period): what came in by channel, then that month's refunds, Stripe fees and net.",
        "parameters": [
          {
            "$ref": "#/components/parameters/place_id"
          },
          {
            "$ref": "#/components/parameters/from"
          },
          {
            "$ref": "#/components/parameters/to"
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "One or more rows per facility, each with its place_id.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Revenue"
                      }
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "place_id,...\r\n12,...\r\n"
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/InvalidParameter"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stats/daily": {
      "get": {
        "operationId": "statsDaily",
        "tags": [
          "Figures"
        ],
        "summary": "The schedule, day by day",
        "description": "One row per facility and per day of the period, days without a class included.",
        "parameters": [
          {
            "$ref": "#/components/parameters/place_id"
          },
          {
            "$ref": "#/components/parameters/from"
          },
          {
            "$ref": "#/components/parameters/to"
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "One or more rows per facility, each with its place_id.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Daily"
                      }
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "place_id,...\r\n12,...\r\n"
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/InvalidParameter"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stats/activities": {
      "get": {
        "operationId": "statsActivities",
        "tags": [
          "Figures"
        ],
        "summary": "Per activity",
        "description": "One row per facility and per activity booked or scheduled in the period, the most booked first.",
        "parameters": [
          {
            "$ref": "#/components/parameters/place_id"
          },
          {
            "$ref": "#/components/parameters/from"
          },
          {
            "$ref": "#/components/parameters/to"
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "One or more rows per facility, each with its place_id.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Activities"
                      }
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "place_id,...\r\n12,...\r\n"
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/InvalidParameter"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stats/slots": {
      "get": {
        "operationId": "statsSlots",
        "tags": [
          "Figures"
        ],
        "summary": "Bookings by weekday and by hour",
        "description": "Per facility, 7 rows with group \"weekday\" (value 1 Monday to 7 Sunday), then 24 with group \"hour\" (value 0 to 23, the hour a class starts).",
        "parameters": [
          {
            "$ref": "#/components/parameters/place_id"
          },
          {
            "$ref": "#/components/parameters/from"
          },
          {
            "$ref": "#/components/parameters/to"
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "One or more rows per facility, each with its place_id.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Slots"
                      }
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "place_id,...\r\n12,...\r\n"
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/InvalidParameter"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stats/members": {
      "get": {
        "operationId": "statsMembers",
        "tags": [
          "Figures"
        ],
        "summary": "Members and how often they come",
        "description": "Members now, and how often they booked since `from` (`to` is not used): 8 bookings or more is regular, 1 to 7 occasional, none inactive.",
        "parameters": [
          {
            "$ref": "#/components/parameters/place_id"
          },
          {
            "$ref": "#/components/parameters/from"
          },
          {
            "$ref": "#/components/parameters/to"
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "One or more rows per facility, each with its place_id.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Members"
                      }
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "place_id,...\r\n12,...\r\n"
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/InvalidParameter"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stats/unpaid": {
      "get": {
        "operationId": "statsUnpaid",
        "tags": [
          "Figures"
        ],
        "summary": "What is owed",
        "description": "What members owe now (the back-office To collect list), and the late-payment rate of the membership periods that started in the period.",
        "parameters": [
          {
            "$ref": "#/components/parameters/place_id"
          },
          {
            "$ref": "#/components/parameters/from"
          },
          {
            "$ref": "#/components/parameters/to"
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "One or more rows per facility, each with its place_id.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Unpaid"
                      }
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "place_id,...\r\n12,...\r\n"
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/InvalidParameter"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stats/trials": {
      "get": {
        "operationId": "statsTrials",
        "tags": [
          "Figures"
        ],
        "summary": "Trials and drop-ins",
        "description": "Trial classes and drop-ins whose class fell in the period.",
        "parameters": [
          {
            "$ref": "#/components/parameters/place_id"
          },
          {
            "$ref": "#/components/parameters/from"
          },
          {
            "$ref": "#/components/parameters/to"
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "One or more rows per facility, each with its place_id.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Trials"
                      }
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "place_id,...\r\n12,...\r\n"
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/InvalidParameter"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/classes": {
      "get": {
        "operationId": "listClasses",
        "tags": [
          "Figures"
        ],
        "summary": "Each class on the schedule",
        "description": "Each scheduled class in the period, facility by facility, by start, 500 a page. In JSON the next page's cursor is `next_cursor`; in CSV it is the `x-next-cursor` header, absent on the last page. Ask again with the same parameters and `cursor` set to it.",
        "parameters": [
          {
            "$ref": "#/components/parameters/place_id"
          },
          {
            "$ref": "#/components/parameters/from"
          },
          {
            "$ref": "#/components/parameters/to"
          },
          {
            "$ref": "#/components/parameters/format"
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "The next_cursor of the page before."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of classes.",
            "headers": {
              "x-next-cursor": {
                "schema": {
                  "type": "string"
                },
                "description": "CSV only: the next page's cursor."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Class"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Pass it as `cursor` for the next page; null on the last."
                    }
                  },
                  "required": [
                    "data",
                    "next_cursor"
                  ]
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "place_id,...\r\n12,...\r\n"
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/InvalidParameter"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "openapi",
        "tags": [
          "Meta"
        ],
        "summary": "This document",
        "security": [],
        "responses": {
          "200": {
            "description": "The OpenAPI document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "`Authorization: Bearer wl_\u2026`. The default: use it wherever the caller can send a header."
      },
      "api_key": {
        "type": "apiKey",
        "in": "query",
        "name": "api_key",
        "description": "For Google Sheets' IMPORTDATA, which cannot send a header. Whoever sees the URL holds the key."
      }
    },
    "parameters": {
      "place_id": {
        "name": "place_id",
        "in": "query",
        "schema": {
          "type": "string",
          "pattern": "^[0-9]+(,[0-9]+)*$"
        },
        "example": "12,14",
        "description": "One facility or several, separated by commas. Absent: every facility the key reads. A facility the key does not read is a 403."
      },
      "from": {
        "name": "from",
        "in": "query",
        "schema": {
          "type": "string",
          "format": "date"
        },
        "description": "First day, in the facility's time zone. Default: 29 days before `to`. A period is at most 366 days."
      },
      "to": {
        "name": "to",
        "in": "query",
        "schema": {
          "type": "string",
          "format": "date"
        },
        "description": "Last day, included. Default: today in the facility's time zone."
      },
      "format": {
        "name": "format",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "json",
            "csv"
          ]
        },
        "description": "`csv` for a header row then one line per row, ready for a spreadsheet. Default JSON."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "The key is missing, unknown or revoked.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "unauthorized",
                "message": "Missing or invalid API key."
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "The key reads no open facility (`no_facility`), or not every `place_id` asked (`place_not_allowed`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "place_not_allowed",
                "message": "This key does not read every place_id asked."
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "More than 60 calls in a minute with this key. Wait the `Retry-After` seconds.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "Too many calls: 60 a minute per key."
              }
            }
          }
        },
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            }
          }
        }
      },
      "InvalidParameter": {
        "description": "A parameter is malformed; the message names it.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "invalid_parameter",
                "message": "from must be on or before to."
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "No such facility for this key, or no such path.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "not_found",
                "message": "No such facility for this key."
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "unauthorized",
                  "no_facility",
                  "rate_limited",
                  "place_not_allowed",
                  "invalid_parameter",
                  "not_found"
                ]
              },
              "message": {
                "type": "string",
                "description": "In English, for the developer."
              }
            }
          }
        }
      },
      "Place": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "The facility's id: the place_id of every figure."
          },
          "name": {
            "type": "string",
            "description": "Its name."
          },
          "time_zone": {
            "type": "string",
            "description": "Its IANA time zone: the days of from and to are its days.",
            "example": "Europe/Paris"
          },
          "currency": {
            "type": "string",
            "description": "The currency of every amount in the row, ISO 4217.",
            "example": "EUR"
          }
        },
        "required": [
          "id",
          "name",
          "time_zone",
          "currency"
        ]
      },
      "Summary": {
        "type": "object",
        "properties": {
          "place_id": {
            "type": "integer",
            "description": "The facility the row is about."
          },
          "from": {
            "type": "string",
            "format": "date",
            "description": "First day of the period."
          },
          "to": {
            "type": "string",
            "format": "date",
            "description": "Last day of the period."
          },
          "active_members": {
            "type": "integer",
            "description": "Members with an active or in-grace membership now."
          },
          "mrr_cents": {
            "type": "integer",
            "description": "Monthly recurring revenue now, in cents (a yearly plan counts a twelfth)."
          },
          "bookings": {
            "type": "integer",
            "description": "Bookings for classes in the period, attended or to come; cancellations and no-shows left out."
          },
          "unique_attendees": {
            "type": "integer",
            "description": "Distinct people who booked a class in the period."
          },
          "booked_seats": {
            "type": "integer",
            "description": "Seats booked in the period's scheduled classes."
          },
          "capacity": {
            "type": "integer",
            "description": "Seats offered by the period's scheduled classes."
          },
          "fill_rate": {
            "type": [
              "integer",
              "null"
            ],
            "description": "booked / capacity, in whole percent; null with no seat."
          },
          "collected_cents": {
            "type": "integer",
            "description": "Payments in the period, less the refunds made in it."
          },
          "refunded_cents": {
            "type": "integer",
            "description": "Refunds made in the period (positive)."
          },
          "fees_cents": {
            "type": "integer",
            "description": "Stripe's fees on the period's payments."
          },
          "net_cents": {
            "type": "integer",
            "description": "collected_cents - fees_cents."
          },
          "fees_pending": {
            "type": "integer",
            "description": "Online payments whose Stripe fee is not known yet: net_cents is too high by their fees until it is."
          },
          "currency": {
            "type": "string",
            "description": "The currency of every amount in the row, ISO 4217.",
            "example": "EUR"
          }
        },
        "required": [
          "place_id",
          "from",
          "to",
          "active_members",
          "mrr_cents",
          "bookings",
          "unique_attendees",
          "booked_seats",
          "capacity",
          "fill_rate",
          "collected_cents",
          "refunded_cents",
          "fees_cents",
          "net_cents",
          "fees_pending",
          "currency"
        ]
      },
      "Revenue": {
        "type": "object",
        "properties": {
          "place_id": {
            "type": "integer",
            "description": "The facility the row is about."
          },
          "month": {
            "type": "string",
            "format": "date",
            "description": "First day of the month."
          },
          "card_cents": {
            "type": "integer",
            "description": "Card payments."
          },
          "sepa_cents": {
            "type": "integer",
            "description": "SEPA direct debits."
          },
          "desk_cents": {
            "type": "integer",
            "description": "Payments recorded at the desk (cash, cheque, transfer)."
          },
          "total_cents": {
            "type": "integer",
            "description": "card_cents + sepa_cents + desk_cents: the month's payments, before refunds. /stats/summary's collected_cents takes the refunds off, so the months of a period need not add up to it."
          },
          "refunded_cents": {
            "type": "integer",
            "description": "Refunds made in the month (positive)."
          },
          "fees_cents": {
            "type": "integer",
            "description": "Stripe's fees on the month's payments."
          },
          "net_cents": {
            "type": "integer",
            "description": "The month's payments less refunds, less fees."
          },
          "currency": {
            "type": "string",
            "description": "The currency of every amount in the row, ISO 4217.",
            "example": "EUR"
          }
        },
        "required": [
          "place_id",
          "month",
          "card_cents",
          "sepa_cents",
          "desk_cents",
          "total_cents",
          "refunded_cents",
          "fees_cents",
          "net_cents",
          "currency"
        ]
      },
      "Daily": {
        "type": "object",
        "properties": {
          "place_id": {
            "type": "integer",
            "description": "The facility the row is about."
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "The day, in the facility's time zone."
          },
          "classes": {
            "type": "integer",
            "description": "Classes scheduled that day."
          },
          "capacity": {
            "type": "integer",
            "description": "Seats they offer."
          },
          "booked": {
            "type": "integer",
            "description": "Seats booked."
          },
          "waitlist": {
            "type": "integer",
            "description": "People on a waiting list."
          },
          "fill_rate": {
            "type": [
              "integer",
              "null"
            ],
            "description": "booked / capacity, in whole percent; null with no seat."
          }
        },
        "required": [
          "place_id",
          "date",
          "classes",
          "capacity",
          "booked",
          "waitlist",
          "fill_rate"
        ]
      },
      "Activities": {
        "type": "object",
        "properties": {
          "place_id": {
            "type": "integer",
            "description": "The facility the row is about."
          },
          "activity_id": {
            "type": "integer",
            "description": "The activity."
          },
          "activity_name": {
            "type": "string",
            "description": "Its name, as the facility wrote it."
          },
          "bookings": {
            "type": "integer",
            "description": "Bookings for its classes in the period."
          },
          "classes": {
            "type": "integer",
            "description": "Its scheduled classes."
          },
          "capacity": {
            "type": "integer",
            "description": "Seats they offer."
          },
          "booked": {
            "type": "integer",
            "description": "Seats booked."
          },
          "waitlist": {
            "type": "integer",
            "description": "People on a waiting list."
          },
          "fill_rate": {
            "type": [
              "integer",
              "null"
            ],
            "description": "booked / capacity, in whole percent; null with no seat."
          }
        },
        "required": [
          "place_id",
          "activity_id",
          "activity_name",
          "bookings",
          "classes",
          "capacity",
          "booked",
          "waitlist",
          "fill_rate"
        ]
      },
      "Slots": {
        "type": "object",
        "properties": {
          "place_id": {
            "type": "integer",
            "description": "The facility the row is about."
          },
          "group": {
            "type": "string",
            "description": "\"weekday\" or \"hour\".",
            "enum": [
              "weekday",
              "hour"
            ]
          },
          "value": {
            "type": "integer",
            "description": "The weekday (1-7) or the hour (0-23)."
          },
          "bookings": {
            "type": "integer",
            "description": "Bookings in the period."
          }
        },
        "required": [
          "place_id",
          "group",
          "value",
          "bookings"
        ]
      },
      "Members": {
        "type": "object",
        "properties": {
          "place_id": {
            "type": "integer",
            "description": "The facility the row is about."
          },
          "from": {
            "type": "string",
            "format": "date",
            "description": "Since when the bookings are counted."
          },
          "active_members": {
            "type": "integer",
            "description": "Members with an active or in-grace membership now."
          },
          "regular": {
            "type": "integer",
            "description": "People with 8 bookings or more since `from`, whatever they paid with (a membership, a pack, a single class): the back-office's own count. They may outnumber active_members."
          },
          "occasional": {
            "type": "integer",
            "description": "People with 1 to 7 bookings since `from`, counted the same way."
          },
          "inactive": {
            "type": "integer",
            "description": "Members with an active or in-grace membership and no booking since `from`."
          }
        },
        "required": [
          "place_id",
          "from",
          "active_members",
          "regular",
          "occasional",
          "inactive"
        ]
      },
      "Unpaid": {
        "type": "object",
        "properties": {
          "place_id": {
            "type": "integer",
            "description": "The facility the row is about."
          },
          "owed_cents": {
            "type": "integer",
            "description": "Owed now."
          },
          "owing_members": {
            "type": "integer",
            "description": "Members who owe now."
          },
          "periods_judged": {
            "type": "integer",
            "description": "Membership periods that started in the period, at least 7 days ago (a younger one cannot be late yet). Free periods left out."
          },
          "periods_late": {
            "type": "integer",
            "description": "Of those, the ones still unpaid or paid more than 7 days after they started. One paid by direct debit is never late."
          },
          "late_rate": {
            "type": [
              "integer",
              "null"
            ],
            "description": "periods_late / periods_judged in whole percent; null with none judged."
          },
          "currency": {
            "type": "string",
            "description": "The currency of every amount in the row, ISO 4217.",
            "example": "EUR"
          }
        },
        "required": [
          "place_id",
          "owed_cents",
          "owing_members",
          "periods_judged",
          "periods_late",
          "late_rate",
          "currency"
        ]
      },
      "Trials": {
        "type": "object",
        "properties": {
          "place_id": {
            "type": "integer",
            "description": "The facility the row is about."
          },
          "trials": {
            "type": "integer",
            "description": "Trial classes booked."
          },
          "came": {
            "type": "integer",
            "description": "Trials the person came to."
          },
          "no_show": {
            "type": "integer",
            "description": "Trials the person missed."
          },
          "converted": {
            "type": "integer",
            "description": "Trials followed by a purchase, or marked converted by the box."
          },
          "conversion_rate": {
            "type": [
              "integer",
              "null"
            ],
            "description": "converted / came in whole percent; null when nobody came."
          },
          "drop_ins": {
            "type": "integer",
            "description": "Drop-in classes booked."
          },
          "drop_in_cents": {
            "type": "integer",
            "description": "What the drop-ins paid."
          },
          "currency": {
            "type": "string",
            "description": "The currency of every amount in the row, ISO 4217.",
            "example": "EUR"
          }
        },
        "required": [
          "place_id",
          "trials",
          "came",
          "no_show",
          "converted",
          "conversion_rate",
          "drop_ins",
          "drop_in_cents",
          "currency"
        ]
      },
      "Class": {
        "type": "object",
        "properties": {
          "place_id": {
            "type": "integer",
            "description": "The facility the row is about."
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "The class's day, in the facility's time zone."
          },
          "starts_at": {
            "type": "string",
            "description": "Start, local time, ISO 8601 without offset.",
            "example": "2026-10-29T18:30:00"
          },
          "ends_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "End, local time, ISO 8601 without offset."
          },
          "activity_id": {
            "type": "integer",
            "description": "The activity."
          },
          "activity_name": {
            "type": "string",
            "description": "Its name."
          },
          "capacity": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Seats; null for a class without a limit."
          },
          "booked": {
            "type": "integer",
            "description": "Seats booked."
          },
          "waitlist": {
            "type": "integer",
            "description": "People on the waiting list."
          },
          "coaches": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Who runs it, by the name the schedule shows; coaches on time off left out. One comma-separated cell in CSV."
          }
        },
        "required": [
          "place_id",
          "date",
          "starts_at",
          "ends_at",
          "activity_id",
          "activity_name",
          "capacity",
          "booked",
          "waitlist",
          "coaches"
        ]
      }
    }
  }
}
