{
  "openapi": "3.1.1",
  "info": {
    "title": "TellMeFirst Explore API",
    "version": "1.0.0",
    "description": "Read-only discovery of available, discoverable TellMeFirst events, open by default with optional completed events. No API key or account is required. The API cannot create events, subscribe visitors or send announcements. The /api/v1 path identifies the initial major API contract; incompatible changes require a new major URL.",
    "contact": {
      "email": "hey@tellmefirst.app",
      "url": "https://tellmefirst.app/contact.md"
    }
  },
  "servers": [
    {
      "url": "/",
      "description": "Use the origin serving this specification."
    }
  ],
  "security": [],
  "paths": {
    "/api/v1/events": {
      "get": {
        "operationId": "listExploreEvents",
        "summary": "Browse or search discoverable events",
        "description": "Returns up to 24 events per page. The status filter accepts open (default), completed (completed only), or all (open and completed). The former includeCompleted parameter is rejected with HTTP 422. Popular ranks open events by confirmed active Email plus Wallet subscriptions and completed events by their saved completion subscription count, highest first; zero and unknown completion counts are excluded from Popular. Recent includes zero-count and unknown-count events and orders by creation time, newest first. Popular falls back to Recent when no eligible popular events exist within the selected status; meta.sort reports the actual order. Ties use a stable identifier. Non-discoverable, deleted, suspended and blocked-owner events remain excluded for every status. The optional q parameter matches a literal substring of the event title after trimming whitespace. A nonempty q searches eligible events within the selected status, including zero-count matches, and ranks by subscription count regardless of requested sort; meta.sort is popular. Completed matches use the saved completion total, with unknown totals ranked as zero rather than inferred from live subscriptions. Empty or missing q retains ordinary browsing and its fallback. Limit 60 requests per minute per IP. Pagination reflects live data rather than a frozen snapshot.",
        "parameters": [
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Requested browse ordering. May fall back from Popular to Recent when Popular is empty. A nonempty q always uses subscription-count ranking and reports popular.",
            "schema": {
              "type": "string",
              "enum": [
                "popular",
                "recent"
              ],
              "default": "popular"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Positive 1-based page number. Empty first pages return an empty list; later pages beyond the result set return 404.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter event state: open (default), completed only, or all open and completed events. Applies to browse, search, fallback and pagination.",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "completed",
                "all"
              ],
              "default": "open"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Optional literal title substring, trimmed before matching. Missing, empty or whitespace-only means browse. Percent, underscore and exclamation marks are literal characters. No wildcard, fuzzy or final-update text search.",
            "schema": {
              "type": [
                "string",
                "null"
              ],
              "maxLength": 160,
              "default": ""
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of public events. Responses are not cached.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store so visibility changes apply on the next request."
              },
              "X-Robots-Tag": {
                "schema": {
                  "type": "string"
                },
                "description": "noindex, nofollow"
              },
              "Access-Control-Allow-Origin": {
                "schema": {
                  "type": "string",
                  "const": "*"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventPage"
                }
              }
            }
          },
          "404": {
            "description": "Requested page is beyond the available result set.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid sort or page input.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "errors"
                  ],
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "errors": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Read rate limit exceeded. Retry after the indicated delay.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "PublicEvent": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "public_id",
          "title",
          "url",
          "status",
          "waiting_count",
          "created_at",
          "managed_by",
          "completed_at",
          "initial_url",
          "completion_message",
          "completion_url",
          "completed_subscription_count"
        ],
        "properties": {
          "public_id": {
            "type": "string",
            "pattern": "^[a-z0-9]{10}$",
            "description": "Stable public identifier, not an internal database ID."
          },
          "title": {
            "type": "string",
            "maxLength": 160
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Canonical HTTPS event page on tellmefirst.app."
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "completed"
            ]
          },
          "waiting_count": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Confirmed active Email plus Wallet subscriptions for open events; null for completed events. Counts subscriptions, not unique people."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "managed_by": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "name"
            ],
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Public manager display name, as on the event page; null if no name is stored."
              },
              "profile_url": {
                "type": "string",
                "format": "uri",
                "description": "Canonical public profile URL. Present only when the manager has a published public profile; otherwise omitted."
              }
            }
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Completion timestamp, or null while open. This is not a scheduled event date."
          },
          "initial_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Safe HTTPS initial information link, or null."
          },
          "completion_message": {
            "type": [
              "string",
              "null"
            ],
            "description": "Public final update text on completed events; null while open."
          },
          "completion_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Safe HTTPS final update destination, or null. Always null while open."
          },
          "completed_subscription_count": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Frozen total of confirmed subscriptions at completion. Null while open or when a legacy completed event has no saved total. Zero is a known zero; null is unknown. Never inferred from current subscriptions."
          }
        }
      },
      "EventPage": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "data",
          "meta",
          "links"
        ],
        "properties": {
          "data": {
            "type": "array",
            "maxItems": 24,
            "items": {
              "$ref": "#/components/schemas/PublicEvent"
            }
          },
          "meta": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "sort",
              "q",
              "status",
              "page",
              "per_page",
              "total",
              "last_page"
            ],
            "properties": {
              "sort": {
                "type": "string",
                "enum": [
                  "popular",
                  "recent"
                ]
              },
              "q": {
                "type": "string",
                "maxLength": 160,
                "description": "Normalized title search, or empty string for browse."
              },
              "status": {
                "type": "string",
                "enum": [
                  "open",
                  "completed",
                  "all"
                ],
                "description": "Applied event status filter."
              },
              "page": {
                "type": "integer",
                "minimum": 1
              },
              "per_page": {
                "type": "integer",
                "const": 24
              },
              "total": {
                "type": "integer",
                "minimum": 0
              },
              "last_page": {
                "type": "integer",
                "minimum": 1
              }
            }
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "next",
              "prev"
            ],
            "properties": {
              "next": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Relative same-origin API URL, or null at the boundary."
              },
              "prev": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Relative same-origin API URL, or null at the boundary."
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "message"
        ],
        "properties": {
          "message": {
            "type": "string"
          }
        }
      }
    }
  }
}
