{
  "openapi": "3.1.0",
  "info": {
    "title": "Twinsi Agent API",
    "version": "1.0.0",
    "summary": "Create or sign in to a Twinsi account on a user's behalf and act for them, with no app to install.",
    "description": "HTTP API for AI agents. The human only has to read an SMS verification code off their phone\nand hand it to the agent.\n\nSign-in flow:\n\n1. `POST /v1/auth/phone/start` with the user's phone number in E.164 form. We text a code.\n2. Ask the human for the code.\n3. `POST /v1/auth/phone/verify` with the `challengeId` and the code. The response carries an\n   access token valid for 30 days.\n4. Send `Authorization: Bearer <accessToken>` on every user route. Call\n   `POST /v1/auth/token/refresh` to get a fresh 30-day token whenever convenient; sign in again\n   only if the token expires unused.\n\nThere is no API key: any agent can start a sign-in for a phone number it was given. Abuse is\nbounded by rate limits per phone number, per source IP and globally.\n\nOnboarding, right after sign-in:\n\n1. `GET /v1/me`: `onboarding.missing` lists what is still required (`username`, `gender`).\n2. `GET /v1/usernames/{username}` to check a name, then `PATCH /v1/me` with `username`,\n   `gender` and optionally `firstName` and `lastName`.\n3. Optionally `PUT /v1/me/selfie` with a photo of the human, used for their avatar.\n4. `POST /v1/me/onboarding/complete`.\n\nErrors always have the shape `{ \"error\": { \"code\", \"message\", ... }, \"requestId\" }`.\nBranch on `error.code`, never on the message.\n\nFitness profile, after onboarding (see AGENTS.md for how to build an accurate one):\n\n1. Ask the human, then `PATCH /v1/me/fitness/profile` with `consent`.\n2. `PUT /v1/me/fitness/workouts` and `PUT /v1/me/fitness/days` with the last 30 days.\n3. `GET /v1/me/fitness` for the computed stats and `next`, the checklist.\n4. `PATCH /v1/me/fitness/profile` with the summary, then `POST /v1/me/fitness/profile/complete`.\n",
    "contact": {
      "name": "Twinsi",
      "url": "https://staging.twinsi.app"
    }
  },
  "servers": [
    {
      "url": "https://staging-api.twinsi.app"
    }
  ],
  "tags": [
    {
      "name": "Auth",
      "description": "Phone sign-in and session tokens"
    },
    {
      "name": "User",
      "description": "The signed-in user"
    },
    {
      "name": "Meta",
      "description": "Service documents and liveness"
    },
    {
      "name": "Fitness",
      "description": "Workouts, daily stats and the fitness profile (after onboarding)"
    },
    {
      "name": "Social",
      "description": "Other users, friends and groups (after onboarding)"
    }
  ],
  "paths": {
    "/v1/auth/phone/start": {
      "post": {
        "tags": [
          "Auth"
        ],
        "operationId": "startPhoneVerification",
        "summary": "Send an SMS verification code to a phone number",
        "description": "Starts phone verification. We text a one-time code to the number. The response is the\nsame whether the number already has a Twinsi account or not; `isNewUser` is only revealed\nafter a successful verify.\n\nRate limits apply per phone number (3 per 10 minutes, 10 per day), per source IP and\nglobally. A refused request is not counted and sends no SMS.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PhoneStartRequest"
              },
              "example": {
                "phoneNumber": "+15551234567"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Code sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PhoneStartResponse"
                },
                "example": {
                  "challengeId": "cJG1i8OfVVfqXrHYZWjI",
                  "expiresIn": 600
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/VerifyUnavailable"
          }
        }
      }
    },
    "/v1/auth/phone/verify": {
      "post": {
        "tags": [
          "Auth"
        ],
        "operationId": "verifyPhoneCode",
        "summary": "Check the SMS code and sign the user in",
        "description": "Checks the code. On success the user is signed in (the Twinsi account is created\nif the number is new) and the response carries a 30-day access token.\n\nA challenge is single use, expires 10 minutes after `start` and allows 5 wrong codes.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PhoneVerifyRequest"
              },
              "example": {
                "challengeId": "cJG1i8OfVVfqXrHYZWjI",
                "code": "922717",
                "timezone": "America/Los_Angeles"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionResponse"
                }
              }
            }
          },
          "400": {
            "description": "Wrong code (`invalid_code`, with `attemptsRemaining`) or invalid body (`invalid_request`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "invalid_code",
                    "message": "Wrong code",
                    "attemptsRemaining": 4
                  },
                  "requestId": "0953cea7-b51c-4e8b-b907-5eced9245695"
                }
              }
            }
          },
          "404": {
            "description": "Unknown `challengeId` (`challenge_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "410": {
            "description": "The challenge expired (`challenge_expired`) or was already verified (`challenge_used`); start again",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Too many wrong codes (`too_many_attempts`) or rate limited (`rate_limited`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "502": {
            "$ref": "#/components/responses/VerifyUnavailable"
          }
        }
      }
    },
    "/v1/auth/token/refresh": {
      "post": {
        "tags": [
          "Auth"
        ],
        "operationId": "refreshSession",
        "summary": "Exchange the access token for a fresh 30-day one",
        "description": "Issues a new access token valid for 30 days from now and revokes the one sent. An agent in\nregular use can refresh, say, weekly and never ask the human for another SMS code.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "New token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          }
        }
      }
    },
    "/v1/auth/signout": {
      "post": {
        "tags": [
          "Auth"
        ],
        "operationId": "signOut",
        "summary": "End this session",
        "description": "Revokes the access token sent; it stops working immediately. The user's other sessions are\nnot affected.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "success"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          }
        }
      }
    },
    "/v1/me": {
      "get": {
        "tags": [
          "User"
        ],
        "operationId": "getMe",
        "summary": "The signed-in user's profile and onboarding state",
        "description": "The profile and what onboarding still needs: `onboarding.missing` lists the required fields\nstill unset (`username`, `gender`) and `onboarding.optional` the optional ones not provided yet.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No profile document for this user (`profile_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "User"
        ],
        "operationId": "updateMe",
        "summary": "Set username, names, gender and birth year",
        "description": "Partial update: send any subset. `null` clears `firstName`, `lastName` or `birthYear`; `username` and\n`gender` can't be cleared. A new username is claimed and the old one released at once, in one\ntransaction, so two users never hold one name.\n\nLimits per user: 30 updates per 10 minutes, 5 username changes per day\n(only successful changes count).\n\nThe response is the same as `GET /v1/me`.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProfileUpdate"
              },
              "example": {
                "username": "alex23",
                "firstName": "Alex",
                "lastName": "Tran",
                "gender": "male"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "400": {
            "description": "Invalid body (`invalid_request`) or username (`invalid_username`, with `reason`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No profile document for this user (`profile_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Username taken (`username_taken`, with `suggestions`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "username_taken",
                    "message": "That username is taken",
                    "suggestions": [
                      "alex23",
                      "alex_tran",
                      "alex481"
                    ]
                  },
                  "requestId": "0b3c2c1e-5f0e-4a43-9f36-1d0f5a8d6c11"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/me/selfie": {
      "put": {
        "tags": [
          "User"
        ],
        "operationId": "putSelfie",
        "summary": "Upload or replace the selfie",
        "description": "A photo of the human, used only to generate their Twinsi avatar. Never returned by any route.\n\nSend either the raw image (`Content-Type: image/jpeg`, `image/png` or `image/webp`) or JSON\n`{\"imageBase64\": \"...\"}` (a `data:` URL prefix is accepted). At most 10 MB. HEIC is not\naccepted; convert it to JPEG first.\n\nThe image is not checked for a face. It is rotated upright, stripped of all metadata (GPS\nincluded), shrunk to at most 1024 px on its long side and stored as JPEG. At most 50 megapixels,\nand the short side must be at least 512 px. A new selfie after onboarding re-queues avatar generation.\n\nLimit: 10 uploads per user per day; rejected images don't count.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "image/jpeg": {
              "schema": {
                "type": "string",
                "contentMediaType": "image/jpeg"
              }
            },
            "image/png": {
              "schema": {
                "type": "string",
                "contentMediaType": "image/png"
              }
            },
            "image/webp": {
              "schema": {
                "type": "string",
                "contentMediaType": "image/webp"
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "imageBase64"
                ],
                "properties": {
                  "imageBase64": {
                    "type": "string",
                    "contentEncoding": "base64"
                  },
                  "contentType": {
                    "type": "string",
                    "description": "Informational; the format is read from the bytes."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "selfie"
                  ],
                  "properties": {
                    "selfie": {
                      "$ref": "#/components/schemas/SelfieState"
                    }
                  }
                },
                "example": {
                  "selfie": {
                    "uploaded": true,
                    "uploadedAt": "2026-10-19T18:02:11.412Z",
                    "width": 768,
                    "height": 1024
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or wrongly typed body (`invalid_request`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No profile document for this user (`profile_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "413": {
            "description": "Larger than 10 MB (`payload_too_large`, with `maxBytes`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Not usable (`selfie_rejected`, with `reason`: `unsupported_format`, `too_small` or `too_large`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "User"
        ],
        "operationId": "deleteSelfie",
        "summary": "Delete the selfie",
        "description": "Idempotent. An avatar already generated from the selfie stays; one still waiting to be generated\nfalls back to the default avatar for the gender.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "selfie"
                  ],
                  "properties": {
                    "selfie": {
                      "$ref": "#/components/schemas/SelfieState"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No profile document for this user (`profile_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/onboarding/complete": {
      "post": {
        "tags": [
          "User"
        ],
        "operationId": "completeOnboarding",
        "summary": "Finish onboarding and assign the avatar",
        "description": "Requires `username` and `gender`. Sets `onboardingCompletedAt` and assigns the avatar: generated\nfrom the selfie if there is one (`status: pending`), otherwise the default for the gender\n(`status: ready`). Call it once the human has answered the optional questions too; a selfie\nadded later still works but regenerates the avatar. Repeat calls change nothing.\n\nIf a friend invite was redeemed before onboarding finished, it is applied here and its outcome (a `HeldInviteOutcome`: `status`, `inviter`, `code`) is merged into `invite` next to `held`, which is then null. An invite dropped earlier (revoked or lapsed, noticed by `GET` or `PATCH /v1/me`) is reported here the same way; onboarding completes whatever its outcome. While one is held, `firstName` and `lastName` are required too.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Profile, the same shape as `GET /v1/me`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No profile document for this user (`profile_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Required fields unset (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "onboarding_incomplete",
                    "message": "Set username and gender first",
                    "missing": [
                      "username",
                      "gender"
                    ]
                  },
                  "requestId": "6a1f0c55-2b8e-4f7e-9c0a-3e2d4b5c6d7e"
                }
              }
            }
          }
        }
      }
    },
    "/v1/usernames/{username}": {
      "get": {
        "tags": [
          "User"
        ],
        "operationId": "checkUsername",
        "summary": "Check whether a username is free",
        "description": "Advisory: the name is only claimed by `PATCH /v1/me`, which can still answer `409\nusername_taken` if someone claims it first. Your own current username is reported as available.\n\nLimit: 60 checks per user per 10 minutes.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "username",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive."
          }
        ],
        "responses": {
          "200": {
            "description": "Availability",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsernameAvailability"
                },
                "example": {
                  "username": "alex",
                  "available": false,
                  "reason": "taken",
                  "suggestions": [
                    "alex23",
                    "alex_tran",
                    "alex481"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/health": {
      "get": {
        "tags": [
          "Meta"
        ],
        "operationId": "health",
        "summary": "Liveness",
        "security": [],
        "responses": {
          "200": {
            "description": "Up",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "service": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "ok",
                    "service"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": [
          "Meta"
        ],
        "operationId": "openapi",
        "summary": "This document",
        "security": [],
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/AGENTS.md": {
      "get": {
        "tags": [
          "Meta"
        ],
        "operationId": "agentsGuide",
        "summary": "Usage guide for agents, in Markdown",
        "security": [],
        "responses": {
          "200": {
            "description": "Markdown guide",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/v1/avatars/{uid}/{file}": {
      "get": {
        "tags": [
          "Onboarding"
        ],
        "operationId": "getAvatarImage",
        "summary": "An avatar image",
        "description": "The Twinsi avatar generated from a selfie: a transparent PNG of the character, head to toe, cropped to it. This is the URL in `avatar.url`; there is no need to build it. No account needed (an avatar is public, unlike the selfie). Each image has its own URL, so it can be cached forever.\n",
        "security": [],
        "parameters": [
          {
            "name": "uid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "file",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f-]{36}\\.png$"
            },
            "description": "`{id}.png`"
          }
        ],
        "responses": {
          "200": {
            "description": "The PNG",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "No such avatar, e.g. replaced by a newer one (`avatar_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/fitness/activity-types": {
      "get": {
        "tags": [
          "Fitness"
        ],
        "operationId": "getFitnessVocabulary",
        "summary": "Activity types, providers and profile vocabularies",
        "description": "The canonical `activityType` values (with display names and groups), the `source.provider` values, the\nApple Health `HKWorkoutActivityType` mapping, the daily metrics, and the profile enums. No auth.\n",
        "responses": {
          "200": {
            "description": "Vocabularies",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FitnessVocabulary"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/fitness": {
      "get": {
        "tags": [
          "Fitness"
        ],
        "operationId": "getFitnessOverview",
        "summary": "What's stored, the computed stats, and what to do next",
        "description": "Start here. `next` lists, in order, what the agent should collect or ask the human for; it is empty once\nthe profile is complete. Writes nothing.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Overview",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FitnessOverview"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No profile document for this user (`profile_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Fitness"
        ],
        "operationId": "deleteFitness",
        "summary": "Delete all fitness data and the profile",
        "description": "Removes every workout, every day of stats and the profile. Idempotent. Do it when the human asks.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "deleted"
                  ],
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No profile document for this user (`profile_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/fitness/workouts": {
      "put": {
        "tags": [
          "Fitness"
        ],
        "operationId": "putWorkouts",
        "summary": "Upsert a batch of workouts",
        "description": "Up to 200 workouts per call, at most 1 MB. Each row is validated on its own: a bad row comes back in\n`rejected` with its index and a `reason`, and the rest is stored. Sending the same\n`source.provider` + `source.externalId` again updates that workout. A workout from another app\nthat overlaps one already stored (same type, more than half of the shorter one) is stored with\n`duplicateOf` and left out of stats; it comes back in `duplicates`.\n\nNeeds `workouts` in the profile's `consent` (`409 consent_required` otherwise).\nLimits per user: 30 batches per 10 minutes (refused batches don't count), 5,000 stored workouts.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WorkoutBatch"
              },
              "example": {
                "workouts": [
                  {
                    "activityType": "running",
                    "activityName": "Morning Run",
                    "startedAt": "2026-10-12T07:02:11-07:00",
                    "endedAt": "2026-10-12T07:49:40-07:00",
                    "durationMinutes": 45.2,
                    "distanceMeters": 8120,
                    "activeCalories": 512,
                    "avgHeartRateBpm": 148,
                    "indoor": false,
                    "source": {
                      "provider": "apple_health",
                      "app": "Strava",
                      "device": "Apple Watch",
                      "externalId": "99E9AC05-CA8A-4061-B29C-5ACC0E2DE62B",
                      "activityType": "37"
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Batch result, with the recomputed stats",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkoutBatchResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or query (`invalid_request`, with `issues`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No profile document for this user (`profile_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`) or no consent for workouts (`consent_required`, with `scope`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "413": {
            "description": "Body over 1 MB (`payload_too_large`) or storage limit reached (`too_many_workouts`, with `max`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "tags": [
          "Fitness"
        ],
        "operationId": "listWorkouts",
        "summary": "List stored workouts, newest first",
        "description": "Duplicates are included, with `duplicateOf` set. Pass `nextCursor` back as `cursor` for the next page.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "First local day, inclusive"
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Last local day, inclusive"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "`nextCursor` from the previous page"
          }
        ],
        "responses": {
          "200": {
            "description": "Workouts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "workouts",
                    "nextCursor"
                  ],
                  "properties": {
                    "workouts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Workout"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Null on the last page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed query or unknown `cursor` (`invalid_request`, with `issues`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No profile document for this user (`profile_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/fitness/workouts/{workoutId}": {
      "delete": {
        "tags": [
          "Fitness"
        ],
        "operationId": "deleteWorkout",
        "summary": "Delete one workout",
        "description": "Workouts that duplicated it are judged again against what is left: the oldest stands on its own and the others duplicate it.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "workoutId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-f0-9]{20}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "deleted",
                    "workoutId"
                  ],
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "workoutId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No such workout (`workout_not_found`) or no profile (`profile_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/fitness/days": {
      "put": {
        "tags": [
          "Fitness"
        ],
        "operationId": "putDays",
        "summary": "Upsert a batch of daily stats",
        "description": "Up to 92 days per call, one row per local calendar day. Metrics not sent keep their stored value;\n`null` clears one; the latest write wins. Days after today (in the human's timezone) are refused.\nNeeds `daily_stats` in the profile's `consent`. Limits per user: 30 batches per 10 minutes (refused batches don't count),\n1,500 stored days.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DayBatch"
              },
              "example": {
                "days": [
                  {
                    "day": "2026-10-11",
                    "steps": 9077,
                    "distanceMeters": 4024.6,
                    "activeCalories": 662,
                    "restingHeartRateBpm": 58,
                    "source": "apple_health"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Batch result, with the recomputed stats",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DayBatchResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or query (`invalid_request`, with `issues`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No profile document for this user (`profile_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`) or no consent for daily stats (`consent_required`, with `scope`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "413": {
            "description": "Body over 1 MB (`payload_too_large`) or storage limit reached (`too_many_days`, with `max`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/me/fitness/profile": {
      "get": {
        "tags": [
          "Fitness"
        ],
        "operationId": "getFitnessProfile",
        "summary": "The fitness profile with its computed stats",
        "description": "Before anything is written this is an empty draft.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FitnessProfile"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No profile document for this user (`profile_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Fitness"
        ],
        "operationId": "updateFitnessProfile",
        "summary": "Write the profile, consent and sharing",
        "description": "Partial update: send any subset. `null` clears a field; `schedule` and `basis` merge into what is stored.\n`consent` must be recorded (after the human agreed) before any workout or day write. Changing\n`primaryActivities`, `level`, `traits` or `schedule.setting` on a completed profile rebuilds the\ncharacter. Limit per user: 30 updates per 10 minutes.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FitnessProfileUpdate"
              },
              "example": {
                "summary": "Runs three mornings a week and lifts twice. Training for a half marathon in December.",
                "primaryActivities": [
                  "running",
                  "strength"
                ],
                "level": "intermediate",
                "traits": [
                  "consistent",
                  "early_bird",
                  "outdoors"
                ],
                "schedule": {
                  "daysPerWeek": 5,
                  "timeOfDay": "morning",
                  "setting": "mixed"
                },
                "goals": [
                  "Half marathon in December"
                ],
                "basis": {
                  "windowDays": 30,
                  "providers": [
                    "apple_health"
                  ],
                  "connectorsOffered": [
                    "apple_health"
                  ],
                  "manualOnly": false,
                  "confidence": "high"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FitnessProfile"
                }
              }
            }
          },
          "400": {
            "description": "Invalid body (`invalid_request`, with `issues`), including a highlight's unknown `workoutId`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No profile document for this user (`profile_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/me/fitness/profile/complete": {
      "post": {
        "tags": [
          "Fitness"
        ],
        "operationId": "completeFitnessProfile",
        "summary": "Mark the profile ready and build the character",
        "description": "Requires `summary`, `primaryActivities`, `level`, `consent`, and a workout in the last 90 days unless\n`basis.manualOnly` is true. Read the profile back to the human before calling it. Repeat calls change\nnothing.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FitnessProfile"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No profile document for this user (`profile_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`) or profile fields missing (`profile_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "profile_incomplete",
                    "message": "Set summary, level first",
                    "missing": [
                      "summary",
                      "level"
                    ]
                  },
                  "requestId": "6a1f0c55-2b8e-4f7e-9c0a-3e2d4b5c6d7e"
                }
              }
            }
          }
        }
      }
    },
    "/v1/users/{username}": {
      "get": {
        "tags": [
          "Social"
        ],
        "operationId": "getUser",
        "summary": "Look someone up by username",
        "description": "Their card, how you stand to them, any pending request between you, and their fitness profile at the tier you may see (public, or friend when they are your friend and share with friends). Your own username returns what your friends see.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "username",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Person"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "Unknown username, or not onboarded (`user_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/users/{username}/fitness/workouts": {
      "get": {
        "tags": [
          "Social"
        ],
        "operationId": "listFriendWorkouts",
        "summary": "A friend’s workouts, newest first",
        "description": "Friends only, and only when they share with friends. Duplicates are left out, so a page can be shorter than `limit` while `nextCursor` is set.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "username",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive"
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "First local day, inclusive"
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Last local day, inclusive"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "`nextCursor` from the previous page"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "workouts",
                    "nextCursor"
                  ],
                  "properties": {
                    "workouts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FriendWorkout"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or query (`invalid_request`, with `issues`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "description": "Not friends (`not_friends`), or they don’t share with friends (`not_shared`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Unknown username, or not onboarded (`user_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/users/{username}/fitness/days": {
      "get": {
        "tags": [
          "Social"
        ],
        "operationId": "listFriendDays",
        "summary": "A friend’s daily stats, newest first",
        "description": "Friends only, and only when they share with friends. Resting heart rate and sleep are never shown. The last 30 days by default, at most 92 per request.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "username",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive"
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "First local day, inclusive"
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Last local day, inclusive"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "days",
                    "from",
                    "to"
                  ],
                  "properties": {
                    "days": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FriendDay"
                      }
                    },
                    "from": {
                      "type": "string",
                      "format": "date"
                    },
                    "to": {
                      "type": "string",
                      "format": "date"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or query (`invalid_request`, with `issues`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "description": "Not friends (`not_friends`), or they don’t share with friends (`not_shared`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Unknown username, or not onboarded (`user_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/me/social": {
      "get": {
        "tags": [
          "Social"
        ],
        "operationId": "socialInbox",
        "summary": "What’s waiting: friend requests, group invitations, next steps",
        "description": "Call when a conversation starts; there are no push notifications to agents. Follow `next` in order.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SocialInbox"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/friends": {
      "get": {
        "tags": [
          "Social"
        ],
        "operationId": "listFriends",
        "summary": "Your friends, most recent first",
        "description": "Cards include names.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "`nextCursor` from the previous page"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "friends",
                    "nextCursor"
                  ],
                  "properties": {
                    "friends": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FriendRow"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or query (`invalid_request`, with `issues`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/friends/{username}": {
      "delete": {
        "tags": [
          "Social"
        ],
        "operationId": "unfriend",
        "summary": "Unfriend",
        "description": "Removes the friendship on both sides at once. Idempotent; the other person isn’t told.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "username",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "removed",
                    "username"
                  ],
                  "properties": {
                    "removed": {
                      "type": "boolean",
                      "description": "False when you weren’t friends"
                    },
                    "username": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "That’s you (`invalid_target`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "Unknown username, or not onboarded (`user_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/me/friends/requests": {
      "post": {
        "tags": [
          "Social"
        ],
        "operationId": "sendFriendRequest",
        "summary": "Send a friend request",
        "description": "Ask the human first. If they already asked you, this accepts theirs (`status: accepted`, 200). Requests lapse after 30 days. At most one request to the same person per 7 days, and 20 a day.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "username"
                ],
                "additionalProperties": false,
                "properties": {
                  "username": {
                    "type": "string"
                  },
                  "message": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Optional, at most 140 characters: who is asking, in the human’s words"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "They had asked you: now friends",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "username"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "accepted"
                      ]
                    },
                    "since": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "username": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Request sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "username"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending"
                      ]
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "username": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed body (`invalid_request`) or yourself (`invalid_target`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "Unknown username, or not onboarded (`user_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "`already_friends`, `request_pending` (with `expiresAt`) or `friend_limit_reached` (with `max`, `kind`); onboarding not finished (`onboarding_incomplete`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "tags": [
          "Social"
        ],
        "operationId": "listFriendRequests",
        "summary": "Pending friend requests",
        "description": "Newest first. Lapsed requests are left out.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "direction",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "incoming",
                "outgoing"
              ],
              "default": "incoming"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "direction",
                    "requests"
                  ],
                  "properties": {
                    "direction": {
                      "type": "string",
                      "enum": [
                        "incoming",
                        "outgoing"
                      ]
                    },
                    "requests": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FriendRequestRow"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or query (`invalid_request`, with `issues`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/friends/requests/{username}": {
      "delete": {
        "tags": [
          "Social"
        ],
        "operationId": "withdrawFriendRequest",
        "summary": "Withdraw a request you sent",
        "description": "The other person isn’t told.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "username",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "withdrawn": {
                      "type": "boolean"
                    },
                    "username": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "Unknown username (`user_not_found`) or no pending request (`request_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/me/friends/requests/{username}/accept": {
      "post": {
        "tags": [
          "Social"
        ],
        "operationId": "acceptFriendRequest",
        "summary": "Accept a friend request",
        "description": "Ask the human first. Idempotent once you’re friends.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "username",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "accepted"
                      ]
                    },
                    "username": {
                      "type": "string"
                    },
                    "since": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "Unknown username (`user_not_found`) or no pending request (`request_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Either side has 1,000 friends (`friend_limit_reached`); onboarding not finished (`onboarding_incomplete`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/me/friends/requests/{username}/decline": {
      "post": {
        "tags": [
          "Social"
        ],
        "operationId": "declineFriendRequest",
        "summary": "Decline a friend request",
        "description": "The sender isn’t told; the request just disappears.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "username",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "declined"
                      ]
                    },
                    "username": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "Unknown username (`user_not_found`) or no pending request (`request_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/me/groups": {
      "get": {
        "tags": [
          "Social"
        ],
        "operationId": "listMyGroups",
        "summary": "Your groups, most recently joined first",
        "description": "At most 50.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "groups"
                  ],
                  "properties": {
                    "groups": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "groupId": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "memberCount": {
                            "type": "integer"
                          },
                          "role": {
                            "type": "string",
                            "enum": [
                              "owner",
                              "admin",
                              "member"
                            ]
                          },
                          "joinedAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/groups/invites": {
      "get": {
        "tags": [
          "Social"
        ],
        "operationId": "listGroupInvites",
        "summary": "Pending group invitations",
        "description": "Newest first. Invitations to deleted groups are dropped.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "invites"
                  ],
                  "properties": {
                    "invites": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/GroupInvite"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or query (`invalid_request`, with `issues`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/groups/invites/{groupId}/accept": {
      "post": {
        "tags": [
          "Social"
        ],
        "operationId": "acceptGroupInvite",
        "summary": "Join the group you were invited to",
        "description": "Ask the human first.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No pending invitation (`invite_not_found`) or the group is gone (`group_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "`already_member`, `group_full` or `group_limit_reached` (with `max`); onboarding not finished (`onboarding_incomplete`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/me/groups/invites/{groupId}/decline": {
      "post": {
        "tags": [
          "Social"
        ],
        "operationId": "declineGroupInvite",
        "summary": "Decline a group invitation",
        "description": "Nobody is told.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "declined": {
                      "type": "boolean"
                    },
                    "groupId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No pending invitation (`invite_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/groups": {
      "post": {
        "tags": [
          "Social"
        ],
        "operationId": "createGroup",
        "summary": "Create a group",
        "description": "You become its owner and a join code is minted. 5 a day.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/GroupInput"
                  },
                  {
                    "required": [
                      "name"
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or query (`invalid_request`, with `issues`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "409": {
            "description": "You are in 50 groups (`group_limit_reached`); onboarding not finished (`onboarding_incomplete`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/groups/{groupId}": {
      "get": {
        "tags": [
          "Social"
        ],
        "operationId": "getGroup",
        "summary": "A group you’re in",
        "description": "Includes the join code.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No such group, or you are not in it (`group_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Social"
        ],
        "operationId": "updateGroup",
        "summary": "Edit a group",
        "description": "Admins and the owner. Send any subset.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GroupInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or query (`invalid_request`, with `issues`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "description": "Your role is too low (`insufficient_role`, with `required`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such group, or you are not in it (`group_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "Social"
        ],
        "operationId": "deleteGroup",
        "summary": "Delete a group",
        "description": "Owner only. Every membership goes with it.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "groupId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "description": "Your role is too low (`insufficient_role`, with `required`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such group, or you are not in it (`group_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/groups/{groupId}/members": {
      "get": {
        "tags": [
          "Social"
        ],
        "operationId": "listGroupMembers",
        "summary": "The roster",
        "description": "Longest-standing first. Names appear only for your friends.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "`nextCursor` from the previous page"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "members",
                    "nextCursor"
                  ],
                  "properties": {
                    "members": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/GroupMember"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or query (`invalid_request`, with `issues`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "No such group, or you are not in it (`group_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/groups/{groupId}/members/{username}": {
      "patch": {
        "tags": [
          "Social"
        ],
        "operationId": "setGroupRole",
        "summary": "Change a member’s role",
        "description": "Owner only. `owner` transfers ownership: they become owner and you an admin.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "username",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "role"
                ],
                "additionalProperties": false,
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "owner",
                      "admin",
                      "member"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupMember"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body (`invalid_request`) or yourself (`invalid_target`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "description": "Your role is too low (`insufficient_role`, with `required`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such group (`group_not_found`) or not a member (`user_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "Social"
        ],
        "operationId": "removeGroupMember",
        "summary": "Remove a member, or leave",
        "description": "Your own username leaves the group. Admins remove members; the owner also removes admins. When the owner leaves, the longest-standing admin (else member) becomes owner; when the last member leaves, the group is deleted.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "username",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "removed",
                    "groupDeleted",
                    "newOwnerUid"
                  ],
                  "properties": {
                    "removed": {
                      "type": "boolean",
                      "description": "False when they weren’t a member"
                    },
                    "groupDeleted": {
                      "type": "boolean"
                    },
                    "newOwnerUid": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "groupId": {
                      "type": "string"
                    },
                    "username": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "description": "Your role is too low (`insufficient_role`, with `required`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such group (`group_not_found`) or unknown username (`user_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/groups/{groupId}/invites": {
      "post": {
        "tags": [
          "Social"
        ],
        "operationId": "inviteToGroup",
        "summary": "Invite someone by username",
        "description": "Any member, or admins only when `whoCanInvite` is `admins`. Inviting someone already invited is a no-op (`already_invited`, 200). 50 a day.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "username"
                ],
                "additionalProperties": false,
                "properties": {
                  "username": {
                    "type": "string"
                  },
                  "message": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Optional, at most 140 characters: who is asking, in the human’s words"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Already invited",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "already_invited"
                      ]
                    },
                    "expiresAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "groupId": {
                      "type": "string"
                    },
                    "username": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Invited",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "invited"
                      ]
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "groupId": {
                      "type": "string"
                    },
                    "username": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed body (`invalid_request`) or yourself (`invalid_target`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "description": "Your role is too low (`insufficient_role`, with `required`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such group (`group_not_found`) or unknown username (`user_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "`already_member` or `group_full`; onboarding not finished (`onboarding_incomplete`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/groups/{groupId}/code/rotate": {
      "post": {
        "tags": [
          "Social"
        ],
        "operationId": "rotateGroupCode",
        "summary": "Replace the join code",
        "description": "Admins. The old code stops working at once.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "description": "Your role is too low (`insufficient_role`, with `required`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such group, or you are not in it (`group_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/groups/{groupId}/code/disable": {
      "post": {
        "tags": [
          "Social"
        ],
        "operationId": "disableGroupCode",
        "summary": "Switch the join code off",
        "description": "Admins. Only invitations let people in until it’s rotated again.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "description": "Your role is too low (`insufficient_role`, with `required`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such group, or you are not in it (`group_not_found`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/groups/codes/{code}": {
      "get": {
        "tags": [
          "Social"
        ],
        "operationId": "previewGroupCode",
        "summary": "What a join code opens",
        "description": "Confirm with the human before joining.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "With or without the hyphen, any case"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "groupId",
                    "name",
                    "memberCount"
                  ],
                  "properties": {
                    "groupId": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "description": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "memberCount": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "Unknown or disabled code (`invalid_join_code`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`, with `missing`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/groups/join": {
      "post": {
        "tags": [
          "Social"
        ],
        "operationId": "joinGroup",
        "summary": "Join a group by its code",
        "description": "A code is standing consent from the group: no approval step. Every attempt counts against 20 per 10 minutes.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "code"
                ],
                "additionalProperties": false,
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "e.g. `7KQ4-M2XN`; case and hyphen don’t matter"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or query (`invalid_request`, with `issues`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "Unknown or disabled code (`invalid_join_code`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "`already_member`, `group_full` or `group_limit_reached` (with `max`); onboarding not finished (`onboarding_incomplete`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/me/invites": {
      "post": {
        "tags": [
          "Social"
        ],
        "operationId": "createInvite",
        "summary": "Invite a friend who isn’t on Twinsi yet",
        "description": "Mints a code and a link the human sends themselves. Whoever redeems it sends the human a friend request showing their full name; nobody becomes friends until the human accepts. The link shows the human’s name and avatar to whoever opens it. 30 days, 20 redemptions, 20 active invites, 20 new invites a day.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "label": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "1–40 characters: who it is for (\"Sam\"); shown back to the human only"
                  },
                  "message": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "≤ 140 characters, shown to whoever previews the code"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The invite",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedInvite"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "409": {
            "description": "20 active invites (`invite_limit_reached`, with `max`); onboarding not finished (`onboarding_incomplete`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "tags": [
          "Social"
        ],
        "operationId": "listInvites",
        "summary": "The human’s unexpired invites",
        "description": "Newest first, used-up ones included; `uses` says how many people redeemed each.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Invites",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "invites"
                  ],
                  "properties": {
                    "invites": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Invite"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/me/invites/{code}": {
      "delete": {
        "tags": [
          "Social"
        ],
        "operationId": "revokeInvite",
        "summary": "Revoke an invite",
        "description": "Its link stops working at once. Requests already made through it stay. Idempotent.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "e.g. `7KQ4-M2XN`; case and hyphen don’t matter"
          }
        ],
        "responses": {
          "200": {
            "description": "Whether there was one",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "removed"
                  ],
                  "properties": {
                    "removed": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "409": {
            "description": "Onboarding not finished (`onboarding_incomplete`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/me/invites/redeem": {
      "post": {
        "tags": [
          "Social"
        ],
        "operationId": "redeemInvite",
        "summary": "Redeem a friend invite",
        "description": "Sends the inviter a friend request that shows the human’s first and last name. Works right after sign-in: before onboarding is finished the code is held (202) and applied by `POST /v1/me/onboarding/complete`, and names become required. If the inviter had already asked the human, this accepts it (200). A retry while the request waits returns it again. Every attempt counts against 10 a day per user and 20 per 10 minutes per IP.\n",
        "security": [
          {
            "AccessToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "code"
                ],
                "additionalProperties": false,
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "e.g. `7KQ4-M2XN`; case and hyphen don’t matter"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "`accepted`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RedeemResult"
                }
              }
            }
          },
          "201": {
            "description": "`pending`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RedeemResult"
                }
              }
            }
          },
          "202": {
            "description": "`held`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RedeemResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body (`invalid_request`); the human’s own invite (`invalid_target`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "Unknown or revoked code, or the inviter is gone (`invalid_invite`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "`name_required` (with `missing`), `already_friends`, `friend_limit_reached`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "410": {
            "description": "Lapsed or used up (`invite_expired`, with `reason`: `lapsed` or `used_up`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/invites/{code}": {
      "get": {
        "tags": [
          "Social"
        ],
        "operationId": "previewInvite",
        "summary": "Who an invite is from",
        "description": "No account needed: confirm the invite with the human before signing them up. Shows the inviter’s card with their names and the invite’s message. 30 per 10 minutes per IP.\n",
        "security": [],
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "e.g. `7KQ4-M2XN`; case and hyphen don’t matter"
          }
        ],
        "responses": {
          "200": {
            "description": "The invite",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvitePreview"
                }
              }
            }
          },
          "404": {
            "description": "Unknown or revoked code, or the inviter is gone (`invalid_invite`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "410": {
            "description": "Lapsed or used up (`invite_expired`, with `reason`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "AccessToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "The `accessToken` (`tas_...`) returned by `verify` or `refresh`. Valid 30 days. Required on user routes."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid body (`invalid_request`) or phone number (`invalid_phone`)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "invalid_phone",
                "message": "phoneNumber must be in E.164 format, e.g. +15551234567"
              },
              "requestId": "9e3d3615-85d7-4601-af48-ea3a02bedba2"
            }
          }
        }
      },
      "Unauthenticated": {
        "description": "Access token missing, unknown, revoked or expired (`unauthenticated`); sign in again",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limited (`rate_limited`). Wait `retryAfterSec` (also sent as a `Retry-After` header).",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds to wait"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "Too many requests, retry later",
                "retryAfterSec": 412,
                "scope": "phone"
              },
              "requestId": "155a55f4-361d-4681-adc1-2ac2b0213568"
            }
          }
        }
      },
      "VerifyUnavailable": {
        "description": "SMS verification provider failed (`verify_unavailable`); retry shortly",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      }
    },
    "schemas": {
      "PhoneStartRequest": {
        "type": "object",
        "required": [
          "phoneNumber"
        ],
        "properties": {
          "phoneNumber": {
            "type": "string",
            "description": "E.164 phone number with country code. Spaces, dashes and parentheses are tolerated.",
            "pattern": "^\\+[1-9][0-9 ().-]{5,30}$",
            "examples": [
              "+15551234567"
            ]
          }
        }
      },
      "PhoneStartResponse": {
        "type": "object",
        "required": [
          "challengeId",
          "expiresIn"
        ],
        "properties": {
          "challengeId": {
            "type": "string",
            "description": "Pass back to `verify`."
          },
          "expiresIn": {
            "type": "integer",
            "description": "Seconds until the challenge expires (600 today). Ask the human for the code right away."
          }
        }
      },
      "PhoneVerifyRequest": {
        "type": "object",
        "required": [
          "challengeId",
          "code"
        ],
        "properties": {
          "challengeId": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "The digits from the SMS, as the human read them.",
            "pattern": "^[0-9]{4,10}$"
          },
          "timezone": {
            "type": "string",
            "description": "IANA timezone of the human, if the agent knows it (e.g. `America/Los_Angeles`). Stored on the profile."
          }
        }
      },
      "SessionResponse": {
        "type": "object",
        "required": [
          "uid",
          "accessToken",
          "expiresIn",
          "expiresAt"
        ],
        "properties": {
          "uid": {
            "type": "string",
            "description": "Twinsi user id. Stable across sessions."
          },
          "isNewUser": {
            "type": "boolean",
            "description": "Returned by `verify` only. True if this call created the account."
          },
          "accessToken": {
            "type": "string",
            "description": "Opaque token, `tas_...`. Send as `Authorization: Bearer <accessToken>`. Store it like a password."
          },
          "expiresIn": {
            "type": "integer",
            "description": "Seconds until the token expires (2592000 = 30 days)."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Me": {
        "type": "object",
        "required": [
          "uid",
          "username",
          "gender",
          "selfie",
          "avatar",
          "onboarding",
          "onboardingCompleted",
          "sessionExpiresAt",
          "invite"
        ],
        "properties": {
          "uid": {
            "type": "string"
          },
          "phoneNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "E.164"
          },
          "phoneCountry": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 3166-1 alpha-2"
          },
          "username": {
            "type": [
              "string",
              "null"
            ],
            "description": "Unique, visible to everyone. 3–20 characters `a-z`, `0-9`, `_`, starting with a letter."
          },
          "firstName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Visible to the user's friends only."
          },
          "lastName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Visible to the user's friends only."
          },
          "gender": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Gender"
              },
              {
                "type": "null"
              }
            ],
            "description": "Visible to everyone."
          },
          "birthYear": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Visible to the user's friends only, like their names. Never shown to an inviter or on an invite preview."
          },
          "timezone": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "selfie": {
            "$ref": "#/components/schemas/SelfieState"
          },
          "avatar": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Avatar"
              },
              {
                "type": "null"
              }
            ],
            "description": "Assigned when onboarding completes."
          },
          "onboarding": {
            "type": "object",
            "required": [
              "completed",
              "completedAt",
              "missing",
              "optional"
            ],
            "properties": {
              "completed": {
                "type": "boolean"
              },
              "completedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "missing": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "username",
                    "gender",
                    "firstName",
                    "lastName"
                  ]
                },
                "description": "Required fields still unset. `POST /v1/me/onboarding/complete` refuses until this is empty. `firstName` and `lastName` are required while a friend invite is held (`invite.held`), since the inviter will see them."
              },
              "optional": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "firstName",
                    "lastName",
                    "birthYear",
                    "selfie"
                  ]
                },
                "description": "Optional items not provided yet."
              }
            }
          },
          "onboardingCompletedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "onboardingCompleted": {
            "type": "boolean"
          },
          "sessionExpiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the access token used for this call expires."
          },
          "invite": {
            "type": "object",
            "description": "A friend invite redeemed before onboarding finished (`held`), applied by `POST /v1/me/onboarding/complete`. On that call's response the outcome's `status`, `inviter` and `code` (a `HeldInviteOutcome`) are merged in next to `held`.",
            "required": [
              "held"
            ],
            "properties": {
              "held": {
                "type": [
                  "object",
                  "null"
                ],
                "required": [
                  "code",
                  "inviter"
                ],
                "properties": {
                  "code": {
                    "type": "string"
                  },
                  "inviter": {
                    "$ref": "#/components/schemas/UserCard"
                  }
                }
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "accepted",
                  "already_friends",
                  "expired",
                  "invalid",
                  "failed"
                ]
              },
              "inviter": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/UserCard"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "code": {
                "type": "string",
                "description": "With `failed`: the error code, e.g. `friend_limit_reached`"
              }
            }
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "error",
          "requestId"
        ],
        "properties": {
          "requestId": {
            "type": "string",
            "description": "Quote this when reporting a problem."
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "$ref": "#/components/schemas/ErrorCode"
              },
              "message": {
                "type": "string",
                "description": "Human readable; may change."
              },
              "retryAfterSec": {
                "type": "integer",
                "description": "Present on `rate_limited` and `too_many_attempts`."
              },
              "scope": {
                "type": "string",
                "description": "Which limit tripped on `rate_limited` (`phone`, `ip`, `global`, `user`); on `consent_required`, the consent scope needed (`workouts`, `daily_stats`)."
              },
              "attemptsRemaining": {
                "type": "integer",
                "description": "Present on `invalid_code`."
              },
              "issues": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Present on `invalid_request`."
              },
              "reason": {
                "type": "string",
                "description": "Present on `invalid_username` (`too_short`, `too_long`, `invalid_chars`, `reserved`, `offensive`) and `selfie_rejected` (`unsupported_format`, `too_small`, `too_large`)."
              },
              "suggestions": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Present on `username_taken`: free usernames close to the one asked for."
              },
              "missing": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Present on `onboarding_incomplete` (`username`, `gender`, `onboarding`) and `profile_incomplete` (`summary`, `primaryActivities`, `level`, `consent`, `workouts`)."
              },
              "maxBytes": {
                "type": "integer",
                "description": "Present on `payload_too_large`."
              },
              "max": {
                "type": "integer",
                "description": "Present on `too_many_workouts` and `too_many_days`."
              }
            }
          }
        }
      },
      "ErrorCode": {
        "type": "string",
        "enum": [
          "invalid_request",
          "rate_limited",
          "invalid_phone",
          "challenge_not_found",
          "challenge_expired",
          "challenge_used",
          "profile_not_found",
          "invalid_username",
          "username_taken",
          "onboarding_incomplete",
          "payload_too_large",
          "selfie_rejected",
          "invalid_code",
          "too_many_attempts",
          "unauthenticated",
          "verify_unavailable",
          "avatar_not_found",
          "internal",
          "consent_required",
          "profile_incomplete",
          "workout_not_found",
          "too_many_workouts",
          "too_many_days",
          "user_not_found",
          "invalid_target",
          "not_friends",
          "not_shared",
          "already_friends",
          "request_pending",
          "request_not_found",
          "friend_limit_reached",
          "group_not_found",
          "insufficient_role",
          "already_member",
          "group_full",
          "group_limit_reached",
          "invalid_join_code",
          "invite_not_found",
          "invalid_invite",
          "invite_expired",
          "invite_limit_reached",
          "name_required"
        ]
      },
      "Gender": {
        "type": "string",
        "enum": [
          "male",
          "female"
        ]
      },
      "SelfieState": {
        "type": "object",
        "required": [
          "uploaded",
          "uploadedAt"
        ],
        "properties": {
          "uploaded": {
            "type": "boolean"
          },
          "uploadedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "width": {
            "type": "integer",
            "description": "Stored width in pixels (upload response only)."
          },
          "height": {
            "type": "integer",
            "description": "Stored height in pixels (upload response only)."
          }
        }
      },
      "Avatar": {
        "type": "object",
        "required": [
          "source",
          "status",
          "defaultId",
          "url",
          "width",
          "height",
          "headshot",
          "failureReason",
          "updatedAt"
        ],
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "selfie",
              "default"
            ],
            "description": "`selfie`: generated from the selfie. `default`: the default avatar for the gender."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "ready",
              "failed"
            ],
            "description": "`pending` while an avatar is being generated from the selfie (usually under a minute), then `ready`, or `failed` (see `failureReason`)."
          },
          "defaultId": {
            "type": [
              "string",
              "null"
            ],
            "description": "`default_male` or `default_female` when `source` is `default`."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "The generated avatar, a transparent PNG (`GET /v1/avatars/{uid}/{file}`), once `ready` from a selfie. `null` for a default avatar and while `pending` or `failed`."
          },
          "width": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Pixel size of the image at `url`."
          },
          "height": {
            "type": [
              "integer",
              "null"
            ]
          },
          "headshot": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/AvatarImage"
              },
              {
                "type": "null"
              }
            ],
            "description": "The 256×256 headshot (transparent PNG, `GET /v1/avatars/{uid}/{file}`), made from the generated avatar a little after it is `ready` (usually under a minute). `null` until then, for a default avatar, and if it couldn't be made: fall back to `url`."
          },
          "failureReason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "refused",
              "error",
              null
            ],
            "description": "When `failed`: `refused`, the image model declined this selfie (another photo may work); `error`, generation kept failing. Uploading a new selfie starts over."
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "AvatarImage": {
        "type": "object",
        "required": [
          "url",
          "width",
          "height"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "width": {
            "type": "integer",
            "description": "Pixel size of the image at `url`."
          },
          "height": {
            "type": "integer"
          }
        }
      },
      "ProfileUpdate": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "username": {
            "type": "string",
            "description": "Lowercased on save; nothing else is rewritten. 3–20 characters `a-z`, `0-9`, `_`, starting with a letter. Can't be cleared."
          },
          "firstName": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 50,
            "description": "Trimmed; letters, spaces, apostrophes, hyphens and dots. `null` clears."
          },
          "lastName": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 50,
            "description": "Same rules as `firstName`."
          },
          "gender": {
            "$ref": "#/components/schemas/Gender"
          },
          "birthYear": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1900,
            "description": "Four-digit year, 1900 to the current year. Only the human's friends see it. `null` clears."
          }
        }
      },
      "UsernameAvailability": {
        "type": "object",
        "required": [
          "username",
          "available"
        ],
        "properties": {
          "username": {
            "type": "string",
            "description": "The name as it would be stored (lowercased)."
          },
          "available": {
            "type": "boolean",
            "description": "Advisory: only `PATCH /v1/me` claims the name."
          },
          "reason": {
            "type": "string",
            "enum": [
              "taken",
              "reserved",
              "invalid"
            ],
            "description": "Absent when available."
          },
          "detail": {
            "type": "string",
            "enum": [
              "too_short",
              "too_long",
              "invalid_chars",
              "offensive"
            ],
            "description": "Present when `reason` is `invalid`."
          },
          "suggestions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Free names close to the one asked for, when it isn't available."
          }
        }
      },
      "WorkoutSource": {
        "type": "object",
        "required": [
          "provider"
        ],
        "properties": {
          "provider": {
            "type": "string",
            "description": "See `GET /v1/fitness/activity-types` `providers`."
          },
          "app": {
            "type": "string",
            "maxLength": 60,
            "description": "The app that recorded it (Apple Health `sourceName`). Required with provider `other`."
          },
          "device": {
            "type": "string",
            "maxLength": 60
          },
          "externalId": {
            "type": "string",
            "maxLength": 128,
            "description": "The provider's id for the session; with `provider` it makes re-sends idempotent."
          },
          "activityType": {
            "type": "string",
            "maxLength": 60,
            "description": "The provider's raw type, e.g. the HKWorkoutActivityType number."
          }
        }
      },
      "WorkoutInput": {
        "type": "object",
        "required": [
          "activityType",
          "startedAt",
          "endedAt",
          "source"
        ],
        "properties": {
          "activityType": {
            "type": "string",
            "description": "Canonical type; see `GET /v1/fitness/activity-types`."
          },
          "activityName": {
            "type": "string",
            "maxLength": 60,
            "description": "Required with activityType `other`."
          },
          "startedAt": {
            "type": "string",
            "format": "date-time",
            "description": "With the local UTC offset, e.g. 2026-10-12T07:02:11-07:00. `Z` is read in the human's timezone."
          },
          "endedAt": {
            "type": "string",
            "format": "date-time"
          },
          "durationMinutes": {
            "type": "number",
            "description": "Active time (pauses excluded). Defaults to the span; a value under 1 minute is read as unknown and the span is used; capped at the span and 12 hours.",
            "minimum": 0
          },
          "distanceMeters": {
            "type": "number",
            "minimum": 0
          },
          "activeCalories": {
            "type": "number",
            "minimum": 0,
            "description": "kcal, active energy (not total)."
          },
          "avgHeartRateBpm": {
            "type": "number",
            "minimum": 20,
            "maximum": 250
          },
          "maxHeartRateBpm": {
            "type": "number",
            "minimum": 20,
            "maximum": 250
          },
          "elevationGainMeters": {
            "type": "number",
            "minimum": 0
          },
          "steps": {
            "type": "number",
            "minimum": 0
          },
          "indoor": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "intensity": {
            "type": "string",
            "enum": [
              "easy",
              "moderate",
              "hard"
            ]
          },
          "notes": {
            "type": "string",
            "maxLength": 500,
            "description": "The human's own words. Never shown to friends."
          },
          "source": {
            "$ref": "#/components/schemas/WorkoutSource"
          }
        }
      },
      "WorkoutBatch": {
        "type": "object",
        "required": [
          "workouts"
        ],
        "additionalProperties": false,
        "properties": {
          "workouts": {
            "type": "array",
            "minItems": 1,
            "maxItems": 200,
            "items": {
              "$ref": "#/components/schemas/WorkoutInput"
            }
          }
        }
      },
      "Workout": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WorkoutInput"
          },
          {
            "type": "object",
            "required": [
              "workoutId",
              "day",
              "durationMinutes",
              "duplicateOf"
            ],
            "properties": {
              "workoutId": {
                "type": "string"
              },
              "day": {
                "type": "string",
                "format": "date",
                "description": "Local calendar day of startedAt."
              },
              "duplicateOf": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "workoutId of the older record this one duplicates."
              },
              "createdAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "updatedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          }
        ]
      },
      "RowRejection": {
        "type": "object",
        "required": [
          "index",
          "reason",
          "message"
        ],
        "properties": {
          "index": {
            "type": "integer"
          },
          "reason": {
            "type": "string",
            "enum": [
              "missing_field",
              "invalid_type",
              "invalid_value",
              "ends_before_start",
              "too_short",
              "too_long",
              "out_of_window"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "WorkoutBatchResult": {
        "type": "object",
        "required": [
          "accepted",
          "created",
          "updated",
          "rejected",
          "duplicates",
          "workoutIds",
          "stats"
        ],
        "properties": {
          "accepted": {
            "type": "integer"
          },
          "created": {
            "type": "integer"
          },
          "updated": {
            "type": "integer"
          },
          "rejected": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RowRejection"
            }
          },
          "duplicates": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer"
                },
                "workoutId": {
                  "type": "string"
                },
                "duplicateOf": {
                  "type": "string"
                },
                "app": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "workoutIds": {
            "type": "array",
            "items": {
              "type": [
                "string",
                "null"
              ]
            },
            "description": "Per input row; null for a rejected row."
          },
          "stats": {
            "$ref": "#/components/schemas/FitnessStats"
          }
        }
      },
      "DayInput": {
        "type": "object",
        "required": [
          "day"
        ],
        "description": "At least one metric. `null` clears a stored metric.",
        "properties": {
          "day": {
            "type": "string",
            "format": "date"
          },
          "steps": {
            "type": [
              "number",
              "null"
            ]
          },
          "distanceMeters": {
            "type": [
              "number",
              "null"
            ],
            "description": "All-day walking and running distance."
          },
          "activeCalories": {
            "type": [
              "number",
              "null"
            ]
          },
          "totalCalories": {
            "type": [
              "number",
              "null"
            ]
          },
          "floorsClimbed": {
            "type": [
              "number",
              "null"
            ]
          },
          "activeMinutes": {
            "type": [
              "number",
              "null"
            ]
          },
          "restingHeartRateBpm": {
            "type": [
              "number",
              "null"
            ],
            "description": "Self only, never shown to friends."
          },
          "sleepMinutes": {
            "type": [
              "number",
              "null"
            ],
            "description": "Self only, never shown to friends."
          },
          "source": {
            "type": "string",
            "description": "Provider for every metric in the row."
          },
          "sources": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "description": "See `GET /v1/fitness/activity-types` `providers`."
            },
            "description": "Provider per metric."
          }
        }
      },
      "DayBatch": {
        "type": "object",
        "required": [
          "days"
        ],
        "additionalProperties": false,
        "properties": {
          "days": {
            "type": "array",
            "minItems": 1,
            "maxItems": 92,
            "items": {
              "$ref": "#/components/schemas/DayInput"
            }
          }
        }
      },
      "DayBatchResult": {
        "type": "object",
        "required": [
          "accepted",
          "created",
          "updated",
          "rejected",
          "stats"
        ],
        "properties": {
          "accepted": {
            "type": "integer"
          },
          "created": {
            "type": "integer"
          },
          "updated": {
            "type": "integer"
          },
          "rejected": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RowRejection"
            }
          },
          "stats": {
            "$ref": "#/components/schemas/FitnessStats"
          }
        }
      },
      "FitnessStats": {
        "type": "object",
        "description": "Computed by Twinsi over the last 30 local days; duplicates excluded.",
        "properties": {
          "window": {
            "type": "object",
            "properties": {
              "from": {
                "type": "string",
                "format": "date"
              },
              "to": {
                "type": "string",
                "format": "date"
              },
              "days": {
                "type": "integer"
              }
            }
          },
          "coverage": {
            "type": "object",
            "properties": {
              "statDays": {
                "type": "integer"
              },
              "firstWorkoutAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "lastWorkoutAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "workouts": {
            "type": "integer"
          },
          "activeDays": {
            "type": "integer"
          },
          "workoutsPerWeek": {
            "type": "number"
          },
          "minutesPerWeek": {
            "type": "integer"
          },
          "consistency": {
            "type": "number",
            "description": "Share of the window's 7-day blocks with a workout."
          },
          "byActivity": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "activityType": {
                  "type": "string"
                },
                "workouts": {
                  "type": "integer"
                },
                "minutes": {
                  "type": "integer"
                },
                "distanceMeters": {
                  "type": "integer"
                }
              }
            }
          },
          "distanceMeters": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Total over the window; null when no workout in it recorded a distance (unknown, not zero)."
          },
          "activeCalories": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Null when no workout in the window recorded active calories."
          },
          "longest": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "workoutId": {
                "type": "string"
              },
              "activityType": {
                "type": "string"
              },
              "minutes": {
                "type": "integer"
              },
              "distanceMeters": {
                "type": "integer"
              }
            }
          },
          "timeOfDay": {
            "type": "object",
            "description": "Workouts by local start hour: morning 5–11, midday 11–14, evening 14–21, night otherwise.",
            "properties": {
              "morning": {
                "type": "integer"
              },
              "midday": {
                "type": "integer"
              },
              "evening": {
                "type": "integer"
              },
              "night": {
                "type": "integer"
              }
            }
          },
          "daysOfWeek": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "minItems": 7,
            "maxItems": 7,
            "description": "Monday first."
          },
          "steps": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "perDay": {
                "type": "integer"
              },
              "days": {
                "type": "integer"
              }
            }
          },
          "restingHeartRateBpm": {
            "type": [
              "number",
              "null"
            ]
          },
          "providers": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "computedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "FitnessProfileUpdate": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "summary": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 20,
            "maxLength": 600,
            "description": "For friends: third person, no name."
          },
          "primaryActivities": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "maxItems": 3
          },
          "level": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "getting_started",
              "beginner",
              "intermediate",
              "advanced",
              "competitive",
              null
            ]
          },
          "traits": {
            "type": [
              "array",
              "null"
            ],
            "maxItems": 5,
            "items": {
              "type": "string"
            },
            "description": "From `profile.traits` in the vocabulary."
          },
          "schedule": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "daysPerWeek": {
                "type": "integer",
                "minimum": 0,
                "maximum": 7
              },
              "timeOfDay": {
                "type": "string",
                "enum": [
                  "morning",
                  "midday",
                  "evening",
                  "night",
                  "varies"
                ]
              },
              "setting": {
                "type": "string",
                "enum": [
                  "gym",
                  "home",
                  "outdoors",
                  "studio",
                  "mixed"
                ]
              }
            }
          },
          "goals": {
            "type": [
              "array",
              "null"
            ],
            "maxItems": 3,
            "items": {
              "type": "string",
              "maxLength": 80
            }
          },
          "highlights": {
            "type": [
              "array",
              "null"
            ],
            "maxItems": 3,
            "items": {
              "type": "object",
              "required": [
                "text"
              ],
              "properties": {
                "text": {
                  "type": "string",
                  "maxLength": 120
                },
                "workoutId": {
                  "type": "string"
                }
              }
            }
          },
          "selfDescribed": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 300,
            "description": "The human's words, verbatim. Self only."
          },
          "basis": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "windowDays": {
                "type": "integer"
              },
              "providers": {
                "type": "array",
                "items": {
                  "type": "string",
                  "description": "See `GET /v1/fitness/activity-types` `providers`."
                }
              },
              "connectorsOffered": {
                "type": "array",
                "items": {
                  "type": "string",
                  "description": "See `GET /v1/fitness/activity-types` `providers`."
                }
              },
              "manualOnly": {
                "type": "boolean"
              },
              "confidence": {
                "type": "string",
                "enum": [
                  "low",
                  "medium",
                  "high"
                ]
              }
            }
          },
          "sharing": {
            "type": "object",
            "required": [
              "friends"
            ],
            "properties": {
              "friends": {
                "type": "boolean"
              }
            }
          },
          "consent": {
            "type": [
              "object",
              "null"
            ],
            "required": [
              "scope"
            ],
            "properties": {
              "scope": {
                "type": "array",
                "minItems": 1,
                "items": {
                  "type": "string",
                  "enum": [
                    "workouts",
                    "daily_stats"
                  ]
                }
              }
            },
            "description": "Record only after the human agreed. `null` withdraws it (stored data stays; use DELETE /v1/me/fitness)."
          }
        }
      },
      "FitnessProfile": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "ready"
            ]
          },
          "completedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "summary": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 20,
            "maxLength": 600,
            "description": "For friends: third person, no name."
          },
          "primaryActivities": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "maxItems": 3
          },
          "level": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "getting_started",
              "beginner",
              "intermediate",
              "advanced",
              "competitive",
              null
            ]
          },
          "traits": {
            "type": [
              "array",
              "null"
            ],
            "maxItems": 5,
            "items": {
              "type": "string"
            },
            "description": "From `profile.traits` in the vocabulary."
          },
          "schedule": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "daysPerWeek": {
                "type": "integer",
                "minimum": 0,
                "maximum": 7
              },
              "timeOfDay": {
                "type": "string",
                "enum": [
                  "morning",
                  "midday",
                  "evening",
                  "night",
                  "varies"
                ]
              },
              "setting": {
                "type": "string",
                "enum": [
                  "gym",
                  "home",
                  "outdoors",
                  "studio",
                  "mixed"
                ]
              }
            }
          },
          "goals": {
            "type": [
              "array",
              "null"
            ],
            "maxItems": 3,
            "items": {
              "type": "string",
              "maxLength": 80
            }
          },
          "highlights": {
            "type": [
              "array",
              "null"
            ],
            "maxItems": 3,
            "items": {
              "type": "object",
              "required": [
                "text"
              ],
              "properties": {
                "text": {
                  "type": "string",
                  "maxLength": 120
                },
                "workoutId": {
                  "type": "string"
                }
              }
            }
          },
          "selfDescribed": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 300,
            "description": "The human's words, verbatim. Self only."
          },
          "basis": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "windowDays": {
                "type": "integer"
              },
              "providers": {
                "type": "array",
                "items": {
                  "type": "string",
                  "description": "See `GET /v1/fitness/activity-types` `providers`."
                }
              },
              "connectorsOffered": {
                "type": "array",
                "items": {
                  "type": "string",
                  "description": "See `GET /v1/fitness/activity-types` `providers`."
                }
              },
              "manualOnly": {
                "type": "boolean"
              },
              "confidence": {
                "type": "string",
                "enum": [
                  "low",
                  "medium",
                  "high"
                ]
              }
            }
          },
          "sharing": {
            "type": "object",
            "required": [
              "friends"
            ],
            "properties": {
              "friends": {
                "type": "boolean"
              }
            }
          },
          "consent": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "scope": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "grantedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "character": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "status": {
                "type": "string"
              },
              "requestedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "stats": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/FitnessStats"
              },
              {
                "type": "null"
              }
            ]
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "FitnessOverview": {
        "type": "object",
        "required": [
          "profile",
          "workouts",
          "days",
          "stats",
          "next"
        ],
        "properties": {
          "profile": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string"
              },
              "completedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "sharing": {
                "type": "object"
              },
              "consent": {
                "type": [
                  "object",
                  "null"
                ]
              }
            }
          },
          "workouts": {
            "type": "object",
            "properties": {
              "count": {
                "type": "integer"
              },
              "from": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "to": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "providers": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Every provider workouts were ever sent from."
              },
              "duplicates": {
                "type": "integer"
              }
            }
          },
          "days": {
            "type": "object",
            "properties": {
              "count": {
                "type": "integer"
              },
              "from": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "to": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "fields": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "stats": {
            "$ref": "#/components/schemas/FitnessStats"
          },
          "next": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "step",
                "why"
              ],
              "properties": {
                "step": {
                  "type": "string",
                  "enum": [
                    "ask_consent",
                    "import_workouts",
                    "import_days",
                    "ask_untracked_sports",
                    "ask_goals",
                    "ask_self_description",
                    "write_profile",
                    "confirm_with_human",
                    "complete"
                  ]
                },
                "why": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "FitnessVocabulary": {
        "type": "object",
        "properties": {
          "activityTypes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "group": {
                  "type": "string"
                },
                "distanceBased": {
                  "type": "boolean"
                }
              }
            }
          },
          "providers": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "appleHealth": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "hkWorkoutActivityType": {
                  "type": "integer"
                },
                "activityType": {
                  "type": "string"
                }
              }
            }
          },
          "dayMetrics": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "profile": {
            "type": "object"
          },
          "intensities": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Relation": {
        "type": "object",
        "required": [
          "type",
          "since"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "self",
              "friend",
              "none"
            ]
          },
          "since": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When you became friends"
          }
        }
      },
      "UserCard": {
        "type": "object",
        "description": "A person as another user sees them. `firstName` and `lastName` appear only to friends (and to the user themself), to the inviter on a request that came through their invite, and on an invite's inviter. `birthYear` appears only to friends and the user themself.",
        "required": [
          "uid",
          "username",
          "gender",
          "avatar"
        ],
        "properties": {
          "uid": {
            "type": "string"
          },
          "username": {
            "type": "string"
          },
          "gender": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "male",
              "female",
              null
            ]
          },
          "avatar": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Avatar"
              },
              {
                "type": "null"
              }
            ]
          },
          "firstName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Friends, the inviter of a via-invite request, and invite previews only"
          },
          "lastName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Friends, the inviter of a via-invite request, and invite previews only"
          },
          "birthYear": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Friends and the user themself only; never to an inviter or on an invite preview"
          }
        }
      },
      "PublicFitnessStats": {
        "type": "object",
        "description": "Computed stats as others see them. The friend tier adds `timeOfDay`, `daysOfWeek`, `steps`, `activeCalories` and `longest.workoutId`. Recovery data, providers and coverage are never shown.",
        "properties": {
          "window": {
            "type": "object",
            "properties": {
              "from": {
                "type": "string",
                "format": "date"
              },
              "to": {
                "type": "string",
                "format": "date"
              },
              "days": {
                "type": "integer"
              }
            }
          },
          "workouts": {
            "type": "integer"
          },
          "activeDays": {
            "type": "integer"
          },
          "workoutsPerWeek": {
            "type": "number"
          },
          "minutesPerWeek": {
            "type": "number"
          },
          "consistency": {
            "type": "number"
          },
          "byActivity": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "activityType": {
                  "type": "string"
                },
                "workouts": {
                  "type": "integer"
                },
                "minutes": {
                  "type": "integer"
                },
                "distanceMeters": {
                  "type": "integer"
                }
              }
            }
          },
          "distanceMeters": {
            "type": [
              "number",
              "null"
            ],
            "description": "Total over the window; null when no workout in it recorded a distance (unknown, not zero)."
          },
          "longest": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "activityType": {
                "type": "string"
              },
              "minutes": {
                "type": "number"
              },
              "distanceMeters": {
                "type": "number"
              },
              "workoutId": {
                "type": "string",
                "description": "Friend tier only"
              }
            }
          },
          "computedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "timeOfDay": {
            "type": "object",
            "description": "Friend tier only",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "daysOfWeek": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "Friend tier only; Monday first"
          },
          "steps": {
            "type": [
              "object",
              "null"
            ],
            "description": "Friend tier only",
            "properties": {
              "perDay": {
                "type": "number"
              },
              "days": {
                "type": "integer"
              }
            }
          },
          "activeCalories": {
            "type": [
              "number",
              "null"
            ],
            "description": "Friend tier only. Null when no workout in the window recorded active calories."
          }
        }
      },
      "FitnessProfileTier": {
        "type": "object",
        "description": "A ready fitness profile as another user sees it. `public` for anyone; `friend` for friends when the owner shares with friends, adding goals, time of day and the friend-tier stats. Never includes selfDescribed, basis, consent, sharing or character.",
        "required": [
          "tier",
          "status",
          "summary",
          "primaryActivities",
          "level",
          "traits",
          "schedule",
          "highlights",
          "stats"
        ],
        "properties": {
          "tier": {
            "type": "string",
            "enum": [
              "public",
              "friend"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "ready"
            ]
          },
          "completedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "summary": {
            "type": [
              "string",
              "null"
            ]
          },
          "primaryActivities": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "level": {
            "type": [
              "string",
              "null"
            ]
          },
          "traits": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "schedule": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "daysPerWeek": {
                "type": "integer"
              },
              "setting": {
                "type": "string"
              },
              "timeOfDay": {
                "type": "string",
                "description": "Friend tier only"
              }
            }
          },
          "highlights": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "text"
              ],
              "properties": {
                "text": {
                  "type": "string"
                },
                "workoutId": {
                  "type": "string",
                  "description": "Friend tier only"
                }
              }
            }
          },
          "goals": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Friend tier only"
          },
          "stats": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/PublicFitnessStats"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "PendingRequest": {
        "type": [
          "object",
          "null"
        ],
        "required": [
          "direction",
          "via",
          "invite",
          "sentAt",
          "message",
          "expiresAt"
        ],
        "properties": {
          "direction": {
            "type": "string",
            "enum": [
              "incoming",
              "outgoing"
            ]
          },
          "via": {
            "type": "string",
            "enum": [
              "request",
              "invite"
            ],
            "description": "`invite` when the request came through the recipient’s invite link; the recipient then sees the sender’s names."
          },
          "invite": {
            "$ref": "#/components/schemas/RequestInvite"
          },
          "sentAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "message": {
            "type": [
              "string",
              "null"
            ]
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "Person": {
        "allOf": [
          {
            "$ref": "#/components/schemas/UserCard"
          },
          {
            "type": "object",
            "required": [
              "relation",
              "friendRequest",
              "fitness",
              "sharesActivity"
            ],
            "properties": {
              "relation": {
                "$ref": "#/components/schemas/Relation"
              },
              "friendRequest": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/PendingRequest"
                  }
                ],
                "description": "A pending request between you, when not friends; offer to accept an incoming one rather than sending another."
              },
              "fitness": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/FitnessProfileTier"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Null until their profile is ready."
              },
              "sharesActivity": {
                "type": [
                  "boolean",
                  "null"
                ],
                "description": "For friends: whether their workouts and daily stats are readable. Null to non-friends."
              }
            }
          }
        ]
      },
      "FriendWorkout": {
        "type": "object",
        "description": "A workout as friends see it: no `notes`, no `source.externalId`, no duplicates, no `createdAt`/`updatedAt`.",
        "properties": {
          "workoutId": {
            "type": "string"
          },
          "activityType": {
            "type": "string"
          },
          "activityName": {
            "type": [
              "string",
              "null"
            ]
          },
          "startedAt": {
            "type": "string",
            "format": "date-time"
          },
          "endedAt": {
            "type": "string",
            "format": "date-time"
          },
          "day": {
            "type": "string",
            "format": "date"
          },
          "durationMinutes": {
            "type": "number"
          },
          "distanceMeters": {
            "type": "number"
          },
          "activeCalories": {
            "type": "number"
          },
          "avgHeartRateBpm": {
            "type": "number"
          },
          "maxHeartRateBpm": {
            "type": "number"
          },
          "elevationGainMeters": {
            "type": "number"
          },
          "steps": {
            "type": "number"
          },
          "indoor": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "intensity": {
            "type": "string"
          },
          "source": {
            "type": "object",
            "properties": {
              "provider": {
                "type": "string"
              },
              "app": {
                "type": "string"
              },
              "device": {
                "type": "string"
              },
              "activityType": {
                "type": "string"
              }
            }
          }
        }
      },
      "FriendDay": {
        "type": "object",
        "required": [
          "day"
        ],
        "description": "A day of daily stats as friends see it: no resting heart rate, sleep or sources.",
        "properties": {
          "day": {
            "type": "string",
            "format": "date"
          },
          "steps": {
            "type": "number"
          },
          "distanceMeters": {
            "type": "number"
          },
          "activeCalories": {
            "type": "number"
          },
          "totalCalories": {
            "type": "number"
          },
          "floorsClimbed": {
            "type": "number"
          },
          "activeMinutes": {
            "type": "number"
          }
        }
      },
      "FriendRequestRow": {
        "allOf": [
          {
            "$ref": "#/components/schemas/UserCard"
          },
          {
            "type": "object",
            "required": [
              "via",
              "invite",
              "message",
              "sentAt",
              "expiresAt"
            ],
            "properties": {
              "via": {
                "type": "string",
                "enum": [
                  "request",
                  "invite"
                ],
                "description": "`invite` when the request came through the recipient’s invite link; the recipient then sees the sender’s names."
              },
              "invite": {
                "$ref": "#/components/schemas/RequestInvite"
              },
              "message": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "sentAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "expiresAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          }
        ]
      },
      "FriendRow": {
        "allOf": [
          {
            "$ref": "#/components/schemas/UserCard"
          },
          {
            "type": "object",
            "required": [
              "since"
            ],
            "properties": {
              "since": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          }
        ]
      },
      "Group": {
        "type": "object",
        "required": [
          "groupId",
          "name",
          "description",
          "memberCount",
          "whoCanInvite",
          "ownerUid",
          "role",
          "joinCode"
        ],
        "properties": {
          "groupId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "memberCount": {
            "type": "integer"
          },
          "whoCanInvite": {
            "type": "string",
            "enum": [
              "members",
              "admins"
            ]
          },
          "ownerUid": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "member"
            ],
            "description": "Your role"
          },
          "joinCode": {
            "type": [
              "string",
              "null"
            ],
            "description": "e.g. `7KQ4-M2XN`; anyone holding it can join. Null when disabled."
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "GroupInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "description": "2–40 characters after trimming"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "At most 300 characters; null clears"
          },
          "whoCanInvite": {
            "type": "string",
            "enum": [
              "members",
              "admins"
            ],
            "default": "members"
          }
        }
      },
      "GroupMember": {
        "allOf": [
          {
            "$ref": "#/components/schemas/UserCard"
          },
          {
            "type": "object",
            "required": [
              "role",
              "joinedAt"
            ],
            "properties": {
              "role": {
                "type": "string",
                "enum": [
                  "owner",
                  "admin",
                  "member"
                ]
              },
              "joinedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          }
        ]
      },
      "GroupInvite": {
        "type": "object",
        "required": [
          "groupId",
          "name",
          "memberCount",
          "from",
          "message",
          "sentAt",
          "expiresAt"
        ],
        "properties": {
          "groupId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "memberCount": {
            "type": "integer"
          },
          "from": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/UserCard"
              },
              {
                "type": "null"
              }
            ],
            "description": "Who invited you"
          },
          "message": {
            "type": [
              "string",
              "null"
            ]
          },
          "sentAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "SocialInbox": {
        "type": "object",
        "required": [
          "friends",
          "friendRequests",
          "groups",
          "next"
        ],
        "properties": {
          "friends": {
            "type": "object",
            "properties": {
              "count": {
                "type": "integer"
              }
            }
          },
          "friendRequests": {
            "type": "object",
            "properties": {
              "incoming": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/FriendRequestRow"
                },
                "description": "Up to 20"
              },
              "incomingCount": {
                "type": "integer"
              },
              "outgoingCount": {
                "type": "integer"
              }
            }
          },
          "groups": {
            "type": "object",
            "properties": {
              "count": {
                "type": "integer"
              },
              "invites": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/GroupInvite"
                },
                "description": "Up to 20"
              }
            }
          },
          "next": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "step",
                "why"
              ],
              "properties": {
                "step": {
                  "type": "string",
                  "enum": [
                    "answer_friend_requests",
                    "answer_group_invites",
                    "enable_sharing"
                  ]
                },
                "why": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "RequestInvite": {
        "type": [
          "object",
          "null"
        ],
        "description": "On a request made by redeeming an invite: which one, and the inviter’s own label for it.",
        "required": [
          "code",
          "label"
        ],
        "properties": {
          "code": {
            "type": "string",
            "example": "7KQ4-M2XN"
          },
          "label": {
            "type": [
              "string",
              "null"
            ],
            "description": "The inviter’s note on who the link was for; null on the sender’s side"
          }
        }
      },
      "Invite": {
        "type": "object",
        "required": [
          "code",
          "url",
          "label",
          "message",
          "uses",
          "maxUses",
          "createdAt",
          "expiresAt",
          "lastRedeemedAt"
        ],
        "properties": {
          "code": {
            "type": "string",
            "example": "7KQ4-M2XN",
            "description": "Case and hyphen don’t matter when it is sent back"
          },
          "url": {
            "type": "string",
            "example": "https://www.twinsi.app/v2/invite/7KQ4-M2XN"
          },
          "label": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who it is for, in the inviter’s words; shown to the inviter only"
          },
          "message": {
            "type": [
              "string",
              "null"
            ],
            "description": "Shown to whoever previews the code"
          },
          "uses": {
            "type": "integer"
          },
          "maxUses": {
            "type": "integer"
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "lastRedeemedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "CreatedInvite": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Invite"
          },
          {
            "type": "object",
            "required": [
              "shareText"
            ],
            "properties": {
              "shareText": {
                "type": "string",
                "description": "Ready to paste into a text: the inviter’s first name (or username), the link and the code. Never the last name."
              }
            }
          }
        ]
      },
      "InvitePreview": {
        "type": "object",
        "required": [
          "code",
          "inviter",
          "message",
          "expiresAt"
        ],
        "properties": {
          "code": {
            "type": "string"
          },
          "inviter": {
            "$ref": "#/components/schemas/UserCard"
          },
          "message": {
            "type": [
              "string",
              "null"
            ]
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "RedeemResult": {
        "type": "object",
        "required": [
          "status",
          "inviter"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "held",
              "pending",
              "accepted"
            ],
            "description": "`held` (202): onboarding isn’t finished; the code is applied when it is. `pending` (201): a request to the inviter is waiting for them. `accepted` (200): the inviter had already asked; you are friends."
          },
          "inviter": {
            "$ref": "#/components/schemas/UserCard"
          },
          "missing": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "With `held`: what onboarding still needs, names included"
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "since": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "HeldInviteOutcome": {
        "type": "object",
        "required": [
          "status",
          "inviter"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "accepted",
              "already_friends",
              "expired",
              "invalid",
              "failed"
            ]
          },
          "inviter": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/UserCard"
              },
              {
                "type": "null"
              }
            ]
          },
          "code": {
            "type": "string",
            "description": "With `failed`: the error code, e.g. `friend_limit_reached`"
          }
        }
      }
    }
  }
}