{
  "openapi": "3.1.0",
  "info": {
    "title": "Bloom Public API",
    "version": "1.0.0",
    "summary": "Public booking, contact and newsletter endpoints for Bloom.",
    "description": "Bloom is practice management software for UK private therapists and counsellors. This specification covers only the truly public API: the endpoints anyone can call with no credentials and no signed link. That is the public booking page - a therapist's consultation profile, availability and booking - plus the marketing contact form and newsletter signup. Endpoints that read or write a therapist's practice data require a logged-in session, and token-gated flows (client onboarding, cancellation, time suggestions) are reachable only via single-use signed links emailed to a specific client; neither is part of this public specification.",
    "contact": {
      "name": "Bloom support",
      "email": "hello@trybloom.co.uk",
      "url": "https://www.trybloom.co.uk/contact"
    },
    "termsOfService": "https://www.trybloom.co.uk/legal/terms-of-service",
    "license": {
      "name": "Proprietary",
      "url": "https://www.trybloom.co.uk/legal/terms-of-service"
    }
  },
  "servers": [
    {
      "url": "https://api.trybloom.co.uk",
      "description": "Production API"
    }
  ],
  "externalDocs": {
    "description": "Bloom product documentation",
    "url": "https://www.trybloom.co.uk/docs"
  },
  "tags": [
    {
      "name": "Service",
      "description": "Service health."
    },
    {
      "name": "Booking",
      "description": "Public consultation booking on a therapist's booking page."
    },
    {
      "name": "Marketing",
      "description": "Public contact form and newsletter signup."
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "Service"
        ],
        "summary": "Service health check",
        "description": "Liveness probe. Returns 200 with a small JSON body when the API is up.",
        "responses": {
          "200": {
            "description": "The service is healthy.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok"
                      ]
                    }
                  },
                  "required": [
                    "status"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/book/{slug}": {
      "get": {
        "operationId": "getBookingProfile",
        "tags": [
          "Booking"
        ],
        "summary": "Get a therapist's public booking profile",
        "description": "Returns the public profile shown on a therapist's booking page, including the free 15-minute video consultation on offer. The slug is the therapist's public booking handle.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Slug"
          }
        ],
        "responses": {
          "200": {
            "description": "The therapist's public booking profile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingProfile"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/TherapistNotFound"
          }
        }
      }
    },
    "/book/{slug}/availability": {
      "get": {
        "operationId": "getBookingAvailability",
        "tags": [
          "Booking"
        ],
        "summary": "List available consultation slots",
        "description": "Returns available 15-minute consultation start times for each UK calendar day in the requested range. Days with no availability are omitted. The range must span between 0 and 42 days; the start date is clamped forward to the current UK day.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Slug"
          },
          {
            "name": "startDate",
            "in": "query",
            "required": true,
            "description": "Inclusive range start, UK calendar date (YYYY-MM-DD).",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "example": "2026-09-01"
            }
          },
          {
            "name": "endDate",
            "in": "query",
            "required": true,
            "description": "Inclusive range end, UK calendar date (YYYY-MM-DD).",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "example": "2026-09-07"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Available slots keyed by UK calendar date.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Availability"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "404": {
            "$ref": "#/components/responses/TherapistNotFound"
          }
        }
      }
    },
    "/book/{slug}/book": {
      "post": {
        "operationId": "createBooking",
        "tags": [
          "Booking"
        ],
        "summary": "Book a consultation",
        "description": "Requests a 15-minute consultation at the given UK date and wall-clock time. The appointment is created in a PENDING state for the therapist to confirm. Rate limited to 10 requests per minute per IP.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Slug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The consultation was booked and is pending confirmation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "404": {
            "$ref": "#/components/responses/TherapistNotFound"
          },
          "409": {
            "description": "The requested slot is no longer available.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/contact": {
      "post": {
        "operationId": "submitContactForm",
        "tags": [
          "Marketing"
        ],
        "summary": "Submit the contact form",
        "description": "Sends a message to the Bloom team. Rate limited to 5 requests per minute per IP. The website field is an anti-spam honeypot and must be left empty.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/Ok"
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "502": {
            "description": "The message could not be delivered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/newsletter": {
      "post": {
        "operationId": "subscribeNewsletter",
        "tags": [
          "Marketing"
        ],
        "summary": "Subscribe to the newsletter",
        "description": "Adds an email address to the Bloom newsletter. Rate limited to 10 requests per minute per IP.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewsletterRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/Ok"
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "Slug": {
        "name": "slug",
        "in": "path",
        "required": true,
        "description": "The therapist's public booking handle.",
        "schema": {
          "type": "string",
          "example": "jane-smith"
        }
      }
    },
    "responses": {
      "Ok": {
        "description": "The request succeeded.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "ok": {
                  "type": "boolean",
                  "enum": [
                    true
                  ]
                }
              },
              "required": [
                "ok"
              ]
            }
          }
        }
      },
      "ValidationError": {
        "description": "The request body or query was invalid.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TherapistNotFound": {
        "description": "No therapist was found for the given booking handle.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "A standard error envelope. Validation errors add a `details` field.",
        "properties": {
          "error": {
            "type": "string",
            "description": "A human-readable error message."
          },
          "details": {
            "type": "object",
            "description": "Optional field-level validation details.",
            "additionalProperties": true
          },
          "code": {
            "type": "string",
            "description": "Optional stable error code."
          }
        },
        "required": [
          "error"
        ]
      },
      "Consultation": {
        "type": "object",
        "properties": {
          "duration": {
            "type": "integer",
            "description": "Consultation length in minutes.",
            "example": 15
          },
          "fee": {
            "type": "integer",
            "description": "Consultation fee in pence (free consultations are 0).",
            "example": 0
          },
          "type": {
            "type": "string",
            "description": "Consultation format.",
            "example": "Video call"
          }
        },
        "required": [
          "duration",
          "fee",
          "type"
        ]
      },
      "BookingProfile": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The therapist's display name."
          },
          "practiceName": {
            "type": "string",
            "description": "The practice name, if set."
          },
          "slug": {
            "type": "string",
            "description": "The public booking handle."
          },
          "avatarUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "A temporary URL for the therapist's avatar, or null."
          },
          "consultation": {
            "$ref": "#/components/schemas/Consultation"
          }
        },
        "required": [
          "name",
          "slug",
          "consultation"
        ]
      },
      "Availability": {
        "type": "object",
        "properties": {
          "availability": {
            "type": "object",
            "description": "Map of UK calendar date (YYYY-MM-DD) to available start times (HH:MM, UK local). Days with no slots are omitted.",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string",
                "pattern": "^\\d{2}:\\d{2}$",
                "example": "09:30"
              }
            }
          }
        },
        "required": [
          "availability"
        ]
      },
      "BookingRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "firstName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "lastName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "phone": {
            "type": "string",
            "minLength": 1,
            "maxLength": 30
          },
          "date": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "UK calendar date."
          },
          "time": {
            "type": "string",
            "pattern": "^\\d{2}:\\d{2}$",
            "description": "UK wall-clock start time (HH:MM)."
          },
          "message": {
            "type": "string",
            "maxLength": 2000,
            "description": "Optional message to the therapist."
          }
        },
        "required": [
          "firstName",
          "lastName",
          "email",
          "phone",
          "date",
          "time"
        ]
      },
      "BookingCreated": {
        "type": "object",
        "properties": {
          "appointment": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "startsAt": {
                "type": "string",
                "format": "date-time"
              },
              "endsAt": {
                "type": "string",
                "format": "date-time"
              }
            },
            "required": [
              "id",
              "startsAt",
              "endsAt"
            ]
          },
          "client": {
            "type": "object",
            "properties": {
              "firstName": {
                "type": "string"
              }
            },
            "required": [
              "firstName"
            ]
          }
        },
        "required": [
          "appointment",
          "client"
        ]
      },
      "ContactRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 320
          },
          "topic": {
            "type": "string",
            "enum": [
              "General enquiry",
              "Product support",
              "Account or billing",
              "Partnership"
            ]
          },
          "message": {
            "type": "string",
            "minLength": 5,
            "maxLength": 5000
          },
          "website": {
            "type": "string",
            "description": "Anti-spam honeypot; leave empty."
          }
        },
        "required": [
          "name",
          "email",
          "topic",
          "message"
        ]
      },
      "NewsletterRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          }
        },
        "required": [
          "email"
        ]
      }
    }
  }
}