{
  "openapi": "3.1.0",
  "info": {
    "title": "Heart Crystal Dance Academy API",
    "version": "1.0.0",
    "summary": "Class facts and booking links for salsa and bachata classes for adults in Naples, Florida.",
    "description": "A small public API for AI agents and apps. Read the schedule, prices and seasons with getClassInfo, and create a Stripe Checkout link with createCheckout (one person) or createCheckoutForTwo (two people in one payment). Creating a link charges nothing: the person opens it and pays on the Stripe page themselves. Never pay on someone's behalf. Errors are JSON in the RFC 9457 Problem Details format. No authentication is needed. Please do not create checkout links without a real person asking to book.",
    "termsOfService": "https://heartcrystaldance.com/terms",
    "contact": {
      "name": "Heart Crystal Dance Academy",
      "url": "https://heartcrystaldance.com/"
    },
    "license": {
      "name": "Heart Crystal Dance Academy Terms of Service",
      "url": "https://heartcrystaldance.com/terms"
    }
  },
  "servers": [
    {
      "url": "https://heartcrystaldance.com"
    }
  ],
  "security": [],
  "externalDocs": {
    "description": "Facts for AI assistants (llms.txt)",
    "url": "https://heartcrystaldance.com/llms.txt"
  },
  "paths": {
    "/api/info": {
      "get": {
        "operationId": "getClassInfo",
        "summary": "Get schedule, location, prices and seasons",
        "description": "Returns everything needed to answer 'when, where and how much': class days and times, the address, prices for one and two people, the current and next season, and the date of the next class. Read-only.",
        "tags": [
          "classes"
        ],
        "responses": {
          "200": {
            "description": "Class facts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClassInfo"
                }
              }
            }
          },
          "405": {
            "description": "Only GET and HEAD are allowed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/checkout": {
      "get": {
        "operationId": "createCheckout",
        "summary": "Create a booking (payment) link for one person",
        "description": "Creates a Stripe Checkout session for one class or one unlimited month for one person and returns its URL. The person must open the URL and pay; the booking exists only after payment. Fails with season_closed between seasons.",
        "tags": [
          "booking"
        ],
        "parameters": [
          {
            "name": "pack",
            "in": "query",
            "required": true,
            "description": "What to buy: `single` is one class ($20), `monthly` is an unlimited month ($100, from the day of payment through the same date next month).",
            "schema": {
              "$ref": "#/components/schemas/Pack"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Language of the Stripe product name and of the thank-you page. Defaults to ru.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "es"
              ],
              "default": "ru"
            }
          },
          {
            "name": "Accept",
            "in": "header",
            "required": false,
            "description": "Send `application/json` to receive the checkout link as JSON (200). Without it the server answers with a 303 redirect to Stripe, as it does for browsers.",
            "schema": {
              "type": "string",
              "example": "application/json"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Checkout link created (returned when the request prefers application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutLink"
                }
              }
            }
          },
          "303": {
            "description": "Redirect to the Stripe Checkout page (default when JSON is not requested).",
            "headers": {
              "Location": {
                "description": "Stripe Checkout URL.",
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "400": {
            "description": "Unknown or missing pack (code unknown_pack).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Classes are between seasons (code season_closed). The response includes next_season.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The payment page could not be opened right now (code checkout_unavailable).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createCheckoutForTwo",
        "summary": "Create a booking (payment) link for two people in one payment",
        "description": "Creates a Stripe Checkout session for two people: one class each ($40) or an unlimited month each ($200). The payer enters their own details on the Stripe page; the second person's name and US phone are sent here. Both people are added to the class list after payment. The second person does not receive text messages.",
        "tags": [
          "booking"
        ],
        "parameters": [
          {
            "name": "Accept",
            "in": "header",
            "required": false,
            "description": "Send `application/json` to receive the checkout link as JSON (200). Without it the server answers with a 303 redirect to Stripe, as it does for browsers.",
            "schema": {
              "type": "string",
              "example": "application/json"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/ForTwoRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checkout link created (returned when the request prefers application/json).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutLink"
                }
              }
            }
          },
          "303": {
            "description": "Redirect to the Stripe Checkout page (default when JSON is not requested).",
            "headers": {
              "Location": {
                "description": "Stripe Checkout URL.",
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request: unknown_pack, guest_name_required or guest_phone_invalid.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Classes are between seasons (code season_closed).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The payment page could not be opened right now (code checkout_unavailable).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "classes",
      "description": "Facts about the classes."
    },
    {
      "name": "booking",
      "description": "Payment links. Nothing is charged until the person pays on Stripe."
    }
  ],
  "components": {
    "schemas": {
      "Pack": {
        "type": "string",
        "enum": [
          "single",
          "monthly"
        ],
        "description": "single: one class, $20 per person. monthly: unlimited month, $100 per person."
      },
      "ForTwoRequest": {
        "type": "object",
        "required": [
          "party",
          "pack",
          "guest_name",
          "guest_phone"
        ],
        "properties": {
          "party": {
            "type": "string",
            "const": "two",
            "description": "Must be 'two'."
          },
          "pack": {
            "$ref": "#/components/schemas/Pack"
          },
          "lang": {
            "type": "string",
            "enum": [
              "en",
              "ru",
              "es"
            ],
            "default": "ru"
          },
          "guest_name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60,
            "description": "Name of the second person."
          },
          "guest_phone": {
            "type": "string",
            "description": "US mobile number of the second person, 10 digits, any formatting.",
            "examples": [
              "(239) 555-0123"
            ]
          }
        }
      },
      "CheckoutLink": {
        "type": "object",
        "required": [
          "checkout_url",
          "pack",
          "quantity",
          "amount_usd"
        ],
        "properties": {
          "checkout_url": {
            "type": "string",
            "format": "uri",
            "description": "Stripe Checkout page. Give it to the person to open and pay."
          },
          "pack": {
            "$ref": "#/components/schemas/Pack"
          },
          "quantity": {
            "type": "integer",
            "enum": [
              1,
              2
            ],
            "description": "Number of people."
          },
          "amount_usd": {
            "type": "number",
            "description": "Total to pay, in US dollars."
          },
          "note": {
            "type": "string"
          }
        }
      },
      "Season": {
        "type": "object",
        "required": [
          "open",
          "close"
        ],
        "properties": {
          "open": {
            "type": "string",
            "format": "date"
          },
          "close": {
            "type": "string",
            "format": "date"
          }
        }
      },
      "ClassInfo": {
        "type": "object",
        "required": [
          "name",
          "url",
          "phone",
          "location",
          "schedule",
          "today",
          "season_open",
          "seasons",
          "prices",
          "booking"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "phone": {
            "type": "string",
            "description": "Phone and WhatsApp."
          },
          "languages": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "location": {
            "type": "object",
            "properties": {
              "street": {
                "type": "string"
              },
              "city": {
                "type": "string"
              },
              "region": {
                "type": "string"
              },
              "postal_code": {
                "type": "string"
              },
              "country": {
                "type": "string"
              },
              "maps_url": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          "schedule": {
            "type": "object",
            "properties": {
              "days": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "start": {
                "type": "string",
                "description": "HH:MM local time"
              },
              "end": {
                "type": "string",
                "description": "HH:MM local time"
              },
              "timezone": {
                "type": "string"
              },
              "note": {
                "type": "string"
              }
            }
          },
          "today": {
            "type": "string",
            "format": "date",
            "description": "Today's date in Naples, Florida."
          },
          "season_open": {
            "type": "boolean",
            "description": "Whether classes are running and can be booked today."
          },
          "current_season": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Season"
              },
              {
                "type": "null"
              }
            ]
          },
          "seasons": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Season"
            }
          },
          "next_class_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "prices": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "pack": {
                  "$ref": "#/components/schemas/Pack"
                },
                "name": {
                  "type": "string"
                },
                "price_usd": {
                  "type": "number"
                },
                "people": {
                  "type": "integer"
                },
                "valid": {
                  "type": "string"
                }
              }
            }
          },
          "renews_automatically": {
            "type": "boolean"
          },
          "booking": {
            "type": "object",
            "properties": {
              "one_person": {
                "type": "string"
              },
              "two_people": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 Problem Details with two extension members: code and hint.",
        "required": [
          "type",
          "title",
          "status",
          "code",
          "detail"
        ],
        "properties": {
          "type": {
            "type": "string",
            "description": "Always about:blank: the HTTP status carries the meaning."
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "code": {
            "type": "string",
            "enum": [
              "unknown_pack",
              "guest_name_required",
              "guest_phone_invalid",
              "season_closed",
              "checkout_unavailable",
              "method_not_allowed",
              "not_found"
            ]
          },
          "detail": {
            "type": "string"
          },
          "hint": {
            "type": "string",
            "description": "What to change so the request succeeds."
          },
          "next_season": {
            "$ref": "#/components/schemas/Season"
          }
        }
      }
    }
  }
}
