{
  "openapi": "3.1.0",
  "info": {
    "title": "Nolizi Calendar API",
    "version": "1.0.0",
    "description": "Beta owner-authenticated API. Use form encoding for writes. Read the workflow and limitations before using booking actions. No caller-supplied idempotency contract.",
    "contact": {
      "email": "hello@nolizi.com"
    }
  },
  "servers": [
    {
      "url": "https://calendar.nolizi.com/api/v1",
      "description": "Production — writes may send real invitations and update calendars"
    }
  ],
  "externalDocs": {
    "description": "Workflow, examples, error handling and current limitations",
    "url": "https://calendar.nolizi.com/docs/api"
  },
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Discovery"
    },
    {
      "name": "Bookings"
    }
  ],
  "paths": {
    "/event-types": {
      "get": {
        "operationId": "listEventTypes",
        "tags": [
          "Discovery"
        ],
        "summary": "List event types owned by the key holder",
        "responses": {
          "200": {
            "description": "Non-removed event types",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "event_types"
                  ],
                  "properties": {
                    "event_types": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EventType"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, or invalid bearer key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Access or booking refused; edge responses may be non-JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown route or resource not owned by this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Booking attempt limit exceeded. Retry-After is not guaranteed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Calendar/provider unavailable. May return HTML rather than JSON.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "Unexpected service/edge failure. Check status and Content-Type; do not automatically retry writes."
          }
        }
      }
    },
    "/slots": {
      "get": {
        "operationId": "listAvailableSlots",
        "tags": [
          "Discovery"
        ],
        "summary": "Find available slots for an owned event type",
        "description": "Next 14 days, clamped by event date settings. No from/to filters. Availability is rechecked on booking; slots are not held.",
        "parameters": [
          {
            "name": "event_type",
            "in": "query",
            "required": true,
            "description": "An event slug returned by listEventTypes, not schedule_id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Available start/end pairs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "slots"
                  ],
                  "properties": {
                    "slots": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Slot"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, or invalid bearer key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Access or booking refused; edge responses may be non-JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown route or resource not owned by this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Booking attempt limit exceeded. Retry-After is not guaranteed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Calendar/provider unavailable. May return HTML rather than JSON.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "Unexpected service/edge failure. Check status and Content-Type; do not automatically retry writes."
          }
        }
      }
    },
    "/bookings": {
      "get": {
        "operationId": "listUpcomingBookings",
        "tags": [
          "Bookings"
        ],
        "summary": "List up to 100 upcoming confirmed bookings",
        "description": "Owner-scoped, sorted by start time. No pagination, historical filter, or booking-by-ID endpoint. Used for reconciliation after writes.",
        "responses": {
          "200": {
            "description": "Upcoming bookings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "bookings"
                  ],
                  "properties": {
                    "bookings": {
                      "type": "array",
                      "maxItems": 100,
                      "items": {
                        "$ref": "#/components/schemas/Booking"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, or invalid bearer key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Access or booking refused; edge responses may be non-JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown route or resource not owned by this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Booking attempt limit exceeded. Retry-After is not guaranteed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Calendar/provider unavailable. May return HTML rather than JSON.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "Unexpected service/edge failure. Check status and Content-Type; do not automatically retry writes."
          }
        }
      },
      "post": {
        "operationId": "createBooking",
        "tags": [
          "Bookings"
        ],
        "summary": "Create a booking and trigger normal notifications",
        "description": "External side effects. Obtain user authorization. Use a fresh returned slot. No automatic write retry or Idempotency-Key contract. Start with solo events with one location and no required questions or email verification. A 201 is not sufficient: require a non-null booking and reconcile its ID/event/time/attendee via listUpcomingBookings. Advanced flows may produce an ambiguous response.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/CreateBooking"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Booking receipt; may be null in unsupported flows. Verify through the list endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "booking"
                  ],
                  "properties": {
                    "booking": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/Receipt"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid or incomplete booking; generic error, no field-level details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, or invalid bearer key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Access or booking refused; edge responses may be non-JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown route or resource not owned by this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Not booked, duplicate, email verification flow, or unavailable slot. An underlying status of 200 is not proof of booking.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Request too large. May return plain text."
          },
          "429": {
            "description": "Booking attempt limit exceeded. Retry-After is not guaranteed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Calendar/provider unavailable. May return HTML rather than JSON.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "Unexpected service/edge failure. Check status and Content-Type; do not automatically retry writes."
          }
        }
      }
    },
    "/bookings/{booking_id}/cancel": {
      "post": {
        "operationId": "cancelBooking",
        "tags": [
          "Bookings"
        ],
        "summary": "Cancel an owned booking",
        "description": "May notify attendees and cancel associated group/series entries. No body required. Repeating a cancellation of an owned, already-cancelled booking returns cancelled:true. Check the website for group scope if uncertain.",
        "parameters": [
          {
            "name": "booking_id",
            "in": "path",
            "required": true,
            "description": "Exact booking ID from a verified booking. Not an event slug.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cancellation acknowledged",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "cancelled"
                  ],
                  "properties": {
                    "cancelled": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, or invalid bearer key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Access or booking refused; edge responses may be non-JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown route or resource not owned by this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Booking attempt limit exceeded. Retry-After is not guaranteed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Calendar/provider unavailable. May return HTML rather than JSON.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "Unexpected service/edge failure. Check status and Content-Type; do not automatically retry writes."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Full API key from /app/api-keys. Owner-scoped with no granular permission scopes. Keep out of model prompts."
      }
    },
    "schemas": {
      "EventType": {
        "type": "object",
        "required": [
          "schedule_id",
          "slug",
          "title",
          "duration_minutes",
          "scheduling_kind"
        ],
        "properties": {
          "schedule_id": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "duration_minutes": {
            "type": "integer"
          },
          "scheduling_kind": {
            "type": "string",
            "enum": [
              "solo",
              "round_robin",
              "collective"
            ]
          }
        }
      },
      "Slot": {
        "type": "object",
        "required": [
          "start",
          "end"
        ],
        "properties": {
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Receipt": {
        "type": "object",
        "required": [
          "booking_id",
          "starts_at",
          "ends_at"
        ],
        "properties": {
          "booking_id": {
            "type": "string"
          },
          "starts_at": {
            "type": "string",
            "format": "date-time"
          },
          "ends_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Booking": {
        "type": "object",
        "required": [
          "booking_id",
          "starts_at",
          "ends_at",
          "status",
          "booker_name",
          "booker_email",
          "event_type"
        ],
        "properties": {
          "booking_id": {
            "type": "string"
          },
          "starts_at": {
            "type": "string",
            "format": "date-time"
          },
          "ends_at": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "const": "confirmed"
          },
          "booker_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "booker_email": {
            "type": [
              "string",
              "null"
            ]
          },
          "event_type": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "CreateBooking": {
        "type": "object",
        "required": [
          "event_type",
          "start",
          "end",
          "name",
          "email"
        ],
        "properties": {
          "event_type": {
            "type": "string",
            "description": "Owned event slug from event-types."
          },
          "start": {
            "type": "string",
            "format": "date-time",
            "description": "Copy exactly from a fresh slots response."
          },
          "end": {
            "type": "string",
            "format": "date-time",
            "description": "Matching end from the same returned slot."
          },
          "q:booking-note": {
            "type": "string",
            "maxLength": 2000,
            "description": "Optional note shared with the host."
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "booker_tz": {
            "type": "string",
            "default": "UTC",
            "description": "IANA timezone such as America/Chicago."
          },
          "location_option": {
            "type": "string",
            "description": "Advanced: exact location option ID. API does not discover these; use the booking page."
          }
        },
        "additionalProperties": {
          "type": "string"
        },
        "description": "Custom question answers use q:<question_id>. Question IDs/requirements are not discoverable through v1; use the booking page for these events. Unknown fields must not be assumed supported."
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "status": {
            "type": "integer",
            "description": "Underlying form handler status, when supplied; can be 200 even when the API returns 409."
          }
        }
      }
    }
  }
}
