{
  "openapi": "3.1.0",
  "info": {
    "title": "Data Hippo public demo scheduling",
    "version": "1.2.0",
    "description": "Read availability and book a 30-minute founder-led Google Meet demo after explicit attendee approval of the date, time, timezone, name and email. Server agents need no credentials or forged browser headers. Booking requires attendee_confirmed:true and a timestamped UUIDv4 Idempotency-Key. Both dev and prod create real invitations; dev is not a dry run. This grants no access to customer data, platform APIs or administrative calendars. Do not submit health data or secrets.",
    "contact": {
      "email": "contact@datahippo.ai",
      "url": "https://datahippo.ai/contact/"
    },
    "termsOfService": "https://datahippo.ai/terms/"
  },
  "servers": [
    {
      "url": "https://dh-booking-prod.fritz-9f9.workers.dev/v1",
      "description": "Production v1 — real calendar, real invitations. Legacy unversioned routes remain supported."
    }
  ],
  "security": [],
  "components": {
    "schemas": {
      "ApiError": {
        "type": "object",
        "required": [
          "error",
          "code",
          "resolution"
        ],
        "additionalProperties": false,
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable explanation. Do not depend on exact wording."
          },
          "code": {
            "type": "string",
            "enum": [
              "invalid_request",
              "forbidden",
              "not_found",
              "booking_conflict",
              "payload_too_large",
              "unsupported_media_type",
              "rate_limited",
              "scheduling_unavailable",
              "invalid_booking_details",
              "attendee_confirmation_required",
              "invalid_idempotency_key",
              "idempotency_expired",
              "agent_booking_unavailable",
              "idempotency_conflict",
              "booking_unconfirmed"
            ],
            "description": "Machine-readable error classification."
          },
          "resolution": {
            "type": "string",
            "description": "Safe next action. Never automatically replace a booking idempotency key."
          }
        },
        "x-code-resolutions": {
          "invalid_request": "Correct the request fields using the OpenAPI specification.",
          "forbidden": "Use an allowed browser origin or a server client without Origin, Sec-Fetch-Site or Sec-Fetch-Dest. Do not forge headers.",
          "not_found": "Use GET /demo/slots or POST /demo/book; check the method and path.",
          "booking_conflict": "Refresh availability and check for an existing invitation before another booking attempt.",
          "payload_too_large": "Keep the JSON request body within 4096 bytes.",
          "unsupported_media_type": "Send a JSON body with Content-Type: application/json.",
          "rate_limited": "Wait for Retry-After seconds. Retain the same key and payload when retrying a booking.",
          "scheduling_unavailable": "Try availability later. For a booking, check for an invitation and retain the same key and payload; do not create a new key automatically.",
          "invalid_booking_details": "Remove unknown fields and use the documented booking schema.",
          "attendee_confirmation_required": "Obtain explicit attendee approval of the time, timezone, name and email before setting attendee_confirmed to true.",
          "invalid_idempotency_key": "Use current Unix milliseconds, a period and a UUIDv4. Check clock accuracy; retain the key for this booking.",
          "idempotency_expired": "Check for an existing invitation before a new attempt. Never automatically replace an expired key.",
          "agent_booking_unavailable": "Use the website or contact the team; do not bypass durable booking coordination.",
          "idempotency_conflict": "Use the original unchanged payload with this key. Do not automatically create a replacement key.",
          "booking_unconfirmed": "Check for an existing invitation or contact the team. Do not submit with a new key."
        },
        "example": {
          "error": "Too many requests. Please wait a minute.",
          "code": "rate_limited",
          "resolution": "Wait for Retry-After seconds. Retain the same key and payload when retrying a booking."
        }
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "Configured requests per 60-second window, per client IP and Cloudflare location. Not remaining quota or a global exact counter.",
        "schema": {
          "type": "integer",
          "enum": [
            3,
            30
          ]
        }
      },
      "RateLimitPolicy": {
        "description": "Named quota policy (q=requests, w=seconds). Enforcement is distributed/eventually consistent; exact remaining/reset values are not exposed.",
        "schema": {
          "type": "string",
          "examples": [
            "\"availability\";q=30;w=60",
            "\"booking\";q=3;w=60"
          ]
        }
      }
    }
  },
  "externalDocs": {
    "url": "https://datahippo.ai/docs/booking/",
    "description": "Native availability and attendee-approved booking examples; also available at /docs/booking.md"
  },
  "paths": {
    "/demo/book": {
      "post": {
        "operationId": "bookDemo",
        "summary": "Create an attendee-approved demo invitation",
        "description": "Creates a real invitation. Server agents omit Origin, Sec-Fetch-Site and Sec-Fetch-Dest; native fetch Sec-Fetch-Mode is accepted. Obtain explicit attendee approval first. Retain the exact Idempotency-Key and payload; identical retries replay the original result. Never automatically generate another key after a timeout or ambiguous result. Keys expire 24 hours after their timestamp. Booking availability is freshly checked. Keep returned invitee management URLs private.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Current Unix milliseconds, period, UUIDv4. Retain the same value and payload for this booking. Rejects more than 5 minutes in the future or older than 24 hours.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{13}\\.[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "start",
                  "timezone",
                  "name",
                  "email",
                  "attendee_confirmed"
                ],
                "properties": {
                  "start": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Attendee-approved UTC start from available slots."
                  },
                  "timezone": {
                    "type": "string",
                    "maxLength": 80,
                    "description": "Attendee IANA timezone."
                  },
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 254
                  },
                  "organization": {
                    "type": "string",
                    "maxLength": 160
                  },
                  "comments": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Optional non-sensitive context; no health data or secrets."
                  },
                  "attendee_confirmed": {
                    "type": "boolean",
                    "const": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmed booking or replay of the same booking. Management links belong to the attendee; do not publish them.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "booking"
                  ],
                  "properties": {
                    "booking": {
                      "type": "object",
                      "required": [
                        "start"
                      ],
                      "properties": {
                        "start": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "cancel_url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "reschedule_url": {
                          "type": "string",
                          "format": "uri"
                        }
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            }
          },
          "400": {
            "description": "Request rejected or scheduling unavailable. Handle code and resolution; do not blindly retry a booking.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            }
          },
          "403": {
            "description": "Request rejected or scheduling unavailable. Handle code and resolution; do not blindly retry a booking.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Request rejected or scheduling unavailable. Handle code and resolution; do not blindly retry a booking.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Slot unavailable, idempotency_conflict (changed payload), booking_unconfirmed (pending/ambiguous outcome), or idempotency_expired. Read the error. Never generate a new key automatically; check the invitation or contact us for uncertain outcomes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            }
          },
          "413": {
            "description": "Request rejected or scheduling unavailable. Handle code and resolution; do not blindly retry a booking.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            }
          },
          "415": {
            "description": "Request rejected or scheduling unavailable. Handle code and resolution; do not blindly retry a booking.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait for Retry-After; retain the exact key and payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer",
                  "minimum": 1,
                  "example": 60
                }
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            }
          },
          "503": {
            "description": "Request rejected or scheduling unavailable. Handle code and resolution; do not blindly retry a booking.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            }
          }
        }
      }
    },
    "/demo/slots": {
      "get": {
        "operationId": "listDemoAvailability",
        "summary": "Read available demo start times",
        "description": "Read-only. Send after as an ISO 8601 instant and days from 1 to 42. Slots are UTC instants; display in the attendee’s IANA timezone. Respect earliest/latest Unix-millisecond bounds. Calendly controls minimum notice, working hours and calendar conflicts; use returned slots rather than assuming a fixed notice period. Only complete months within 90 days are displayed. Edge availability is reused for less than 3 seconds and is freshly rechecked when booking. Server clients omit Origin and browser Fetch Metadata headers; browser clients must use an allowed website origin. Reads are rate-limited.",
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "days",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 42
            }
          },
          {
            "name": "timezone",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "America/New_York"
            },
            "description": "IANA timezone, such as America/New_York or Europe/London."
          }
        ],
        "responses": {
          "200": {
            "description": "Available start times; this does not reserve a slot.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "slots",
                    "earliest",
                    "latest"
                  ],
                  "properties": {
                    "slots": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "iso"
                        ],
                        "properties": {
                          "iso": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "earliest": {
                      "type": "integer",
                      "description": "Earliest permitted start, Unix milliseconds."
                    },
                    "latest": {
                      "type": "integer",
                      "description": "Latest permitted start, Unix milliseconds."
                    },
                    "mode": {
                      "type": "string",
                      "description": "Server-configured mode. Live creates real invitations."
                    }
                  }
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            }
          },
          "400": {
            "description": "Request rejected or scheduling unavailable. Handle code and resolution; do not blindly retry a booking.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            }
          },
          "403": {
            "description": "Request rejected or scheduling unavailable. Handle code and resolution; do not blindly retry a booking.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Request rejected or scheduling unavailable. Handle code and resolution; do not blindly retry a booking.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit reached. Wait for Retry-After seconds before another availability read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Delay in seconds before retrying; both rate-limit bindings use a 60-second window.",
                "schema": {
                  "type": "integer",
                  "minimum": 1,
                  "example": 60
                }
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            }
          },
          "503": {
            "description": "Request rejected or scheduling unavailable. Handle code and resolution; do not blindly retry a booking.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            }
          }
        }
      }
    }
  }
}