{
  "openapi": "3.0.3",
  "info": {
    "title": "BuddyUp Agent API",
    "description": "Plain HTTP JSON API that lets AI agents sign users up for BuddyUp and act on their behalf (send buddy requests, accept invitations, exchange messages with BuddyUp mobile users). Designed for one-POST signup with plain-English inputs.",
    "version": "1.0.0",
    "termsOfService": "https://letsbuddyup.com/terms",
    "contact": { "email": "support@letsbuddyup.com", "url": "https://letsbuddyup.com" },
    "license": { "name": "Proprietary", "url": "https://letsbuddyup.com/terms" },
    "x-signup-guide": "https://letsbuddyup.com/SIGNUP.md",
    "x-full-guide": "https://letsbuddyup.com/AGENTS.md",
    "x-llms-txt": "https://letsbuddyup.com/llms.txt",
    "x-mcp-endpoint": "https://letsbuddyup.com/api/mcp",
    "x-mcp-manifest": "https://letsbuddyup.com/.well-known/mcp.json",
    "x-browser-fallback-signup-url": "https://letsbuddyup.com/signup-with-ai?email=<user>&name=<display>&location=<city, region>&gender=<male|female|non_binary|prefer_not_to_say>&birthday=<YYYY-MM-DD>&about=<bio>&activities=<a,b,c>&availability=<plain english>&photo=<https url>&agent=<your name>",
    "x-browser-fallback-note": "For LLM runtimes that cannot POST from chat (Gemini, Perplexity, plain Claude web, plain ChatGPT without Actions): DO NOT give the user a curl command, DO NOT tell them to execute anything locally, DO NOT hand off to 'their own agent/server', DO NOT simulate a response. Build the x-browser-fallback-signup-url with URL-encoded query params and paste it into your reply as a clickable link. The user taps it once, the page POSTs /api/v1/register.json on their behalf, and returns the mobile sign-in URL inline."
  },
  "servers": [{ "url": "https://letsbuddyup.com", "description": "Production" }],
  "tags": [
    { "name": "signup", "description": "Create accounts — no auth required." },
    { "name": "profile", "description": "Read / update authenticated profile." },
    { "name": "photo", "description": "Upload profile photos." },
    { "name": "buddies", "description": "Send, accept, decline buddy requests." },
    { "name": "chat", "description": "Send messages and poll for updates." },
    { "name": "users", "description": "Public profile lookup." },
    { "name": "meta", "description": "Sitemap + activity reference." },
    { "name": "events", "description": "Aggregated local-events feed (same data as letsbuddyup.com/events2)." }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Bearer token returned by POST /api/v1/register.json. Use as Authorization: Bearer <token>."
      }
    },
    "schemas": {
      "RegisterRequest": {
        "type": "object",
        "required": ["email", "displayName"],
        "description": "Register a new BuddyUp user in one POST. Required-for-Discover fields (mobile parity): displayName, aboutMe, gender, birthday, location, coords, activities (>=1), schedule/availability, photoURL. If any are missing the account is still created but `onboarding.complete=false` and the profile is hidden from Discover until they're filled in (via PATCH /api/v1/me.json).",
        "properties": {
          "email": { "type": "string", "format": "email" },
          "displayName": { "type": "string", "minLength": 1, "maxLength": 60 },
          "password": {
            "type": "string",
            "minLength": 8,
            "description": "Optional. Omit and the server generates a strong password returned in auth.generated_password once."
          },
          "aboutMe": { "type": "string", "maxLength": 600 },
          "city": {
            "type": "string",
            "maxLength": 80,
            "deprecated": true,
            "description": "Legacy. Prefer `location`. Mirrored into `location` server-side if only city is sent."
          },
          "location": {
            "type": "string",
            "maxLength": 120,
            "description": "Required-for-Discover. Full location label (e.g. 'Oakland, CA'). Mirrored into homeLocation on write."
          },
          "coords": {
            "type": "object",
            "description": "Required-for-Discover radius filters. Falls back to best-effort Nominatim geocoding of `location` when omitted; accuracy varies.",
            "properties": {
              "latitude": { "type": "number", "minimum": -90, "maximum": 90 },
              "longitude": { "type": "number", "minimum": -180, "maximum": 180 }
            },
            "required": ["latitude", "longitude"]
          },
          "gender": {
            "type": "string",
            "enum": ["male", "female", "non_binary", "prefer_not_to_say"],
            "description": "Required-for-Discover. Mirrors mobile onboarding enum."
          },
          "birthday": {
            "type": "string",
            "format": "date",
            "description": "Required-for-Discover. ISO YYYY-MM-DD. Enforces 13+ minimum age."
          },
          "showAgeByDecade": {
            "type": "boolean",
            "description": "Privacy: show age as a decade bucket ('30s') on profile instead of exact age."
          },
          "activities": {
            "type": "array",
            "items": { "type": "string" },
            "maxItems": 20,
            "description": "Plain English activity names. Server canonicalizes. Unknown entries surface in warnings.activities_unmatched."
          },
          "availability_text": {
            "type": "string",
            "maxLength": 500,
            "description": "Plain English availability like 'evenings and Saturday mornings'. 'Weekends' = ALL DAY Sat + ALL DAY Sun."
          },
          "schedule": {
            "description": "Canonical mobile-shape availability. Either a boolean[7][18] grid (day 0=Mon..6=Sun, slot 0=6am..17=11pm) or flat map { '0_0': true, ..., '6_17': true }.",
            "oneOf": [
              {
                "type": "array",
                "items": { "type": "array", "items": { "type": "boolean" }, "minItems": 18, "maxItems": 18 },
                "minItems": 7,
                "maxItems": 7
              },
              { "type": "object", "additionalProperties": { "type": "boolean" } }
            ]
          },
          "photo": {
            "type": "string",
            "description": "Inline base64 or data URL image (JPEG/PNG/WEBP, 5 MB max)."
          },
          "photoMimeType": { "type": "string", "enum": ["image/jpeg", "image/png", "image/webp"] },
          "photoURL": { "type": "string", "format": "uri" },
          "agentName": { "type": "string", "maxLength": 60 },
          "agentSessionId": { "type": "string", "maxLength": 120 },
          "emailMagicLink": {
            "type": "boolean",
            "default": true,
            "description": "Whether to also email the magic sign-in link. The URL is always returned inline as auth.mobile_signin_url."
          }
        }
      },
      "RegisterResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "user": {
            "type": "object",
            "properties": {
              "id": { "type": "string" },
              "email": { "type": "string" },
              "slug": { "type": "string" },
              "profile_url": { "type": "string", "format": "uri" }
            }
          },
          "auth": {
            "type": "object",
            "properties": {
              "token": { "type": "string" },
              "token_type": { "type": "string", "enum": ["Bearer"] },
              "scopes": { "type": "array", "items": { "type": "string" } },
              "generated_password": { "type": "string", "nullable": true },
              "mobile_signin_url": { "type": "string", "nullable": true },
              "mobile_signin_expires_in_seconds": { "type": "integer", "nullable": true },
              "emailed_magic_link": { "type": "boolean" }
            }
          },
          "onboarding": {
            "type": "object",
            "properties": {
              "complete": { "type": "boolean" },
              "missing": { "type": "array", "items": { "type": "string" } }
            }
          },
          "warnings": { "type": "object", "additionalProperties": true },
          "next_action_for_user": { "type": "string" }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": { "type": "string" },
              "message": { "type": "string" },
              "hint": { "type": "string" },
              "fields": { "type": "object", "additionalProperties": { "type": "string" } }
            }
          }
        }
      }
    }
  },
  "paths": {
    "/api/v1/index.json": {
      "get": {
        "tags": ["meta"],
        "summary": "Machine-readable API sitemap",
        "operationId": "getApiSitemap",
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/api/v1/activities.json": {
      "get": {
        "tags": ["meta"],
        "summary": "List canonical activity ids (usually unnecessary — register accepts plain English).",
        "operationId": "listActivities",
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/api/v1/events.json": {
      "get": {
        "tags": ["events"],
        "summary": "List aggregated events (Bay Area and other regions) from the BuddyUp events-directory service.",
        "description": "Public read-only. Proxies to the upstream events API configured via EVENTS_API_URL. Same query parameters as GET /api/events on the directory: limit, offset, region, activities, date_from, date_to, city, q, bbox, sources, sort, etc. Response includes `events`, `total`, and optional `scraped_at_max`.",
        "operationId": "listEvents",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 40 } },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "default": 0 } },
          { "name": "region", "in": "query", "schema": { "type": "string", "example": "bay_area" } },
          { "name": "activities", "in": "query", "schema": { "type": "string", "description": "Comma-separated BuddyUp activity ids" } },
          { "name": "date_from", "in": "query", "schema": { "type": "string", "format": "date" } },
          { "name": "date_to", "in": "query", "schema": { "type": "string", "format": "date" } },
          { "name": "city", "in": "query", "schema": { "type": "string" } },
          { "name": "q", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "events": { "type": "array", "items": { "type": "object" } },
                    "total": { "type": "integer" },
                    "scraped_at_max": { "type": "string", "nullable": true }
                  }
                }
              }
            }
          },
          "502": { "description": "Upstream events service error" },
          "503": { "description": "Events service unreachable" }
        }
      }
    },
    "/api/v1/register.json": {
      "post": {
        "tags": ["signup"],
        "summary": "One-POST signup. Creates an account, returns a bearer token, a one-tap mobile sign-in URL, and auto-generated password (if password omitted).",
        "operationId": "registerUser",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": { "schema": { "$ref": "#/components/schemas/RegisterRequest" } }
          }
        },
        "responses": {
          "201": {
            "description": "Account created",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RegisterResponse" } } }
          },
          "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Email taken", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limited (3/hr/IP).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "get": {
        "tags": ["signup"],
        "summary": "Self-documenting signup instructions + schema.",
        "operationId": "getRegisterDocs",
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/api/v1/me.json": {
      "get": {
        "tags": ["profile"],
        "summary": "Get authenticated profile.",
        "operationId": "getMe",
        "security": [{ "bearerAuth": [] }],
        "responses": { "200": { "description": "OK" } }
      },
      "patch": {
        "tags": ["profile"],
        "summary": "Update authenticated profile. Accepts partial updates and plain-English activities/availability.",
        "operationId": "patchMe",
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "displayName": { "type": "string", "maxLength": 60 },
                  "aboutMe": { "type": "string", "maxLength": 600 },
                  "city": { "type": "string", "maxLength": 80, "deprecated": true },
                  "location": { "type": "string", "maxLength": 120 },
                  "coords": {
                    "type": "object",
                    "properties": {
                      "latitude": { "type": "number" },
                      "longitude": { "type": "number" }
                    }
                  },
                  "gender": {
                    "type": "string",
                    "enum": ["male", "female", "non_binary", "prefer_not_to_say"]
                  },
                  "birthday": { "type": "string", "format": "date" },
                  "showAgeByDecade": { "type": "boolean" },
                  "activities": { "type": "array", "items": { "type": "string" } },
                  "availability_text": { "type": "string", "maxLength": 500 },
                  "schedule": {
                    "oneOf": [
                      { "type": "array", "items": { "type": "array", "items": { "type": "boolean" } } },
                      { "type": "object", "additionalProperties": { "type": "boolean" } }
                    ]
                  },
                  "photoURL": { "type": "string", "format": "uri" }
                }
              }
            }
          }
        },
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/api/v1/me/photo.json": {
      "post": {
        "tags": ["photo"],
        "summary": "Upload a profile photo (base64 or data URL).",
        "operationId": "uploadPhoto",
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["photo"],
                "properties": {
                  "photo": { "type": "string" },
                  "mimeType": { "type": "string", "enum": ["image/jpeg", "image/png", "image/webp"] }
                }
              }
            }
          }
        },
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/api/v1/users/{slug}/profile.json": {
      "get": {
        "tags": ["users"],
        "summary": "Public profile lookup by slug.",
        "operationId": "getUserProfile",
        "parameters": [{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/api/v1/users/{slug}/buddy_request.json": {
      "post": {
        "tags": ["buddies"],
        "summary": "Send a buddy request to another user.",
        "operationId": "sendBuddyRequest",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "message": { "type": "string", "maxLength": 280 },
                  "activityIds": { "type": "array", "items": { "type": "string" }, "maxItems": 5 }
                }
              }
            }
          }
        },
        "responses": { "201": { "description": "Created" } }
      }
    },
    "/api/v1/buddy_requests/{invitationId}/accept.json": {
      "post": {
        "tags": ["buddies"],
        "summary": "Accept a buddy invitation.",
        "operationId": "acceptBuddyRequest",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "name": "invitationId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": { "type": "object", "properties": { "firstMessage": { "type": "string", "maxLength": 600 } } }
            }
          }
        },
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/api/v1/buddy_requests/{invitationId}/decline.json": {
      "post": {
        "tags": ["buddies"],
        "summary": "Decline a buddy invitation.",
        "operationId": "declineBuddyRequest",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "name": "invitationId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/api/v1/inbox.json": {
      "get": {
        "tags": ["chat"],
        "summary": "Primary polling endpoint — returns new messages, pending requests, unread counts.",
        "operationId": "checkInbox",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "name": "since", "in": "query", "schema": { "type": "string", "format": "date-time" } }],
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/api/v1/chats.json": {
      "get": {
        "tags": ["chat"],
        "summary": "List active chats with last-message preview and unread counts.",
        "operationId": "listChats",
        "security": [{ "bearerAuth": [] }],
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/api/v1/chats/{chatId}/messages.json": {
      "get": {
        "tags": ["chat"],
        "summary": "Get messages for a chat.",
        "operationId": "getMessages",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          { "name": "chatId", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "since", "in": "query", "schema": { "type": "string", "format": "date-time" } },
          { "name": "mark_read", "in": "query", "schema": { "type": "boolean" } }
        ],
        "responses": { "200": { "description": "OK" } }
      },
      "post": {
        "tags": ["chat"],
        "summary": "Send a message.",
        "operationId": "sendMessage",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "name": "chatId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["text"],
                "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 4000 } }
              }
            }
          }
        },
        "responses": { "201": { "description": "Created" } }
      }
    },
    "/api/v1/chats/{chatId}/read.json": {
      "post": {
        "tags": ["chat"],
        "summary": "Mark all messages in a chat as read.",
        "operationId": "markChatRead",
        "security": [{ "bearerAuth": [] }],
        "parameters": [{ "name": "chatId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "OK" } }
      }
    }
  }
}
