{
  "openapi": "3.1.1",
  "info": {
    "title": "AlmostThere.Server | v1",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://almostthere.dock.astm.co.za/"
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "tags": [
          "Reference"
        ],
        "summary": "Liveness",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/auth/device": {
      "post": {
        "tags": [
          "Identity"
        ],
        "summary": "Register this device",
        "description": "Creates a user and returns the bearer token the device keeps. No password, no email — an invite link is all anyone needs.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterDeviceRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/me": {
      "get": {
        "tags": [
          "Identity"
        ],
        "summary": "Who this device is",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "patch": {
        "tags": [
          "Identity"
        ],
        "summary": "Change your name or your picture",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateMeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/today": {
      "get": {
        "tags": [
          "Today"
        ],
        "summary": "Everything on today, host and guest side",
        "description": "The one home screen: what you are hosting, what you are attending, and whether your phone is currently sharing anything.",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/events": {
      "post": {
        "tags": [
          "Events"
        ],
        "summary": "Create a visit and get its invite link",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateEventRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/events/{id}": {
      "get": {
        "tags": [
          "Events"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "patch": {
        "tags": [
          "Events"
        ],
        "summary": "Change the visit (host only)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateEventRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/events/{id}/board": {
      "get": {
        "tags": [
          "Events"
        ],
        "summary": "Who is coming, and how far away",
        "description": "The map and bars screens read this. A guest calling it gets names and RSVPs only — distances are disclosed to the host alone.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/events/{id}/alarms": {
      "get": {
        "tags": [
          "Events"
        ],
        "summary": "Arrival alarm settings, event over global",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/events/{id}/alarms/{guestId}": {
      "patch": {
        "tags": [
          "Events"
        ],
        "summary": "Per-guest alarm override",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "guestId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateGuestAlarmRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/events/{id}/arrival-instructions": {
      "get": {
        "tags": [
          "Events"
        ],
        "summary": "The note every guest gets the moment they arrive",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/events/{id}/thanks": {
      "get": {
        "tags": [
          "Events"
        ],
        "summary": "The thank-you that sends itself after the visit",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/events/{id}/thanks/send": {
      "post": {
        "tags": [
          "Events"
        ],
        "summary": "Send the thank-you now instead of waiting",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/events/{id}/invite": {
      "get": {
        "tags": [
          "Events"
        ],
        "summary": "The link to send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/events/{id}/me": {
      "get": {
        "tags": [
          "Being a guest"
        ],
        "summary": "Where you stand at this visit",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "patch": {
        "tags": [
          "Being a guest"
        ],
        "summary": "Change what the host can see",
        "description": "Rejects anything more revealing than the host's suggestion, and drops any stored position the new setting no longer permits.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateGuestPrivacyRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/events/{id}/pings": {
      "post": {
        "tags": [
          "Being a guest"
        ],
        "summary": "Report how far away you are",
        "description": "One number, computed on the device. Coordinates are accepted only while your effective shape is Pin, and the previous reading is overwritten rather than appended — the server holds no track.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PingRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/events/{id}/arrivals": {
      "post": {
        "tags": [
          "Being a guest"
        ],
        "summary": "Tell the host you are here",
        "description": "Stops sharing, deletes the last reading, and returns the note the host left for arrivals.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/events/{id}/arrivals/{guestId}/ack": {
      "post": {
        "tags": [
          "Being a guest"
        ],
        "summary": "Dismiss the arrival alarm (host)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "guestId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/events/{id}/arrivals/pending": {
      "get": {
        "tags": [
          "Being a guest"
        ],
        "summary": "Arrival alarms waiting to be dismissed",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/invites/{token}": {
      "get": {
        "tags": [
          "Invites"
        ],
        "summary": "Read an invite",
        "description": "Anonymous. Locked options are the ones more revealing than the host asked for.",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/invites/{token}/rsvp": {
      "post": {
        "tags": [
          "Invites"
        ],
        "summary": "Answer an invite",
        "description": "Registering a device is one anonymous call, so no account is needed to say yes or no. The chosen preset is checked against the host's suggestion.",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RsvpRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/presets": {
      "get": {
        "tags": [
          "Reference"
        ],
        "summary": "The five presets, plus Custom",
        "description": "Pass ?suggested=<presetId> to get the locked flags for a particular invite.",
        "parameters": [
          {
            "name": "suggested",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/sync": {
      "get": {
        "tags": [
          "Reference"
        ],
        "summary": "Connection and sync state",
        "description": "Reaching this at all is the connection test — a client that cannot reach it renders the offline variant from its own queue.",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/demo/personas": {
      "get": {
        "tags": [
          "Reference"
        ],
        "summary": "The seeded demo accounts",
        "description": "Only populated while demo seeding is on. Lets the web client switch between the host side and the guest side without registering anything.",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "CreateEventRequest": {
        "required": [
          "title",
          "startsAt",
          "endsAt",
          "venueAddress",
          "venueLat",
          "venueLng",
          "suggestedPresetId"
        ],
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "startsAt": {
            "type": "string",
            "format": "date-time"
          },
          "endsAt": {
            "type": [
              "null",
              "string"
            ],
            "format": "date-time"
          },
          "venueAddress": {
            "type": "string"
          },
          "venueLat": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "format": "double"
          },
          "venueLng": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "format": "double"
          },
          "suggestedPresetId": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "PingRequest": {
        "required": [
          "distanceKm",
          "lat",
          "lng",
          "measuredAt"
        ],
        "type": "object",
        "properties": {
          "distanceKm": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "format": "double"
          },
          "lat": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "format": "double"
          },
          "lng": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "format": "double"
          },
          "measuredAt": {
            "type": [
              "null",
              "string"
            ],
            "format": "date-time"
          }
        }
      },
      "RegisterDeviceRequest": {
        "required": [
          "displayName"
        ],
        "type": "object",
        "properties": {
          "displayName": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "RsvpRequest": {
        "required": [
          "status",
          "presetId",
          "customShape",
          "customRevealWithinKm",
          "customTellOnArrival",
          "rememberForHost"
        ],
        "type": "object",
        "properties": {
          "status": {
            "type": "string"
          },
          "presetId": {
            "type": [
              "null",
              "string"
            ]
          },
          "customShape": {
            "type": [
              "null",
              "string"
            ]
          },
          "customRevealWithinKm": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "format": "double"
          },
          "customTellOnArrival": {
            "type": [
              "null",
              "boolean"
            ]
          },
          "rememberForHost": {
            "type": [
              "null",
              "boolean"
            ]
          }
        }
      },
      "UpdateEventRequest": {
        "required": [
          "title",
          "startsAt",
          "venueAddress",
          "suggestedPresetId",
          "alarmFenceMeters",
          "alarmTone",
          "alarmRepeatUntilAcknowledged",
          "alarmVibrate",
          "quietHours",
          "autoOpenArrivalMessage",
          "arrivalMessage",
          "thanksMessage",
          "thanksSchedule",
          "thanksToArrivedOnly"
        ],
        "type": "object",
        "properties": {
          "title": {
            "type": [
              "null",
              "string"
            ]
          },
          "startsAt": {
            "type": [
              "null",
              "string"
            ],
            "format": "date-time"
          },
          "venueAddress": {
            "type": [
              "null",
              "string"
            ]
          },
          "suggestedPresetId": {
            "type": [
              "null",
              "string"
            ]
          },
          "alarmFenceMeters": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "alarmTone": {
            "type": [
              "null",
              "string"
            ]
          },
          "alarmRepeatUntilAcknowledged": {
            "type": [
              "null",
              "boolean"
            ]
          },
          "alarmVibrate": {
            "type": [
              "null",
              "boolean"
            ]
          },
          "quietHours": {
            "type": [
              "null",
              "boolean"
            ]
          },
          "autoOpenArrivalMessage": {
            "type": [
              "null",
              "boolean"
            ]
          },
          "arrivalMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "thanksMessage": {
            "type": [
              "null",
              "string"
            ]
          },
          "thanksSchedule": {
            "type": [
              "null",
              "string"
            ]
          },
          "thanksToArrivedOnly": {
            "type": [
              "null",
              "boolean"
            ]
          }
        }
      },
      "UpdateGuestAlarmRequest": {
        "required": [
          "tone",
          "enabled",
          "repeat"
        ],
        "type": "object",
        "properties": {
          "tone": {
            "type": [
              "null",
              "string"
            ]
          },
          "enabled": {
            "type": [
              "null",
              "boolean"
            ]
          },
          "repeat": {
            "type": [
              "null",
              "boolean"
            ]
          }
        }
      },
      "UpdateGuestPrivacyRequest": {
        "required": [
          "presetId",
          "customShape",
          "customRevealWithinKm",
          "customTellOnArrival",
          "visible",
          "showName",
          "showPhoto",
          "countOnly",
          "rememberForHost"
        ],
        "type": "object",
        "properties": {
          "presetId": {
            "type": [
              "null",
              "string"
            ]
          },
          "customShape": {
            "type": [
              "null",
              "string"
            ]
          },
          "customRevealWithinKm": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "format": "double"
          },
          "customTellOnArrival": {
            "type": [
              "null",
              "boolean"
            ]
          },
          "visible": {
            "type": [
              "null",
              "boolean"
            ]
          },
          "showName": {
            "type": [
              "null",
              "boolean"
            ]
          },
          "showPhoto": {
            "type": [
              "null",
              "boolean"
            ]
          },
          "countOnly": {
            "type": [
              "null",
              "boolean"
            ]
          },
          "rememberForHost": {
            "type": [
              "null",
              "boolean"
            ]
          }
        }
      },
      "UpdateMeRequest": {
        "required": [
          "displayName",
          "photoDataUrl",
          "clearPhoto"
        ],
        "type": "object",
        "properties": {
          "displayName": {
            "type": [
              "null",
              "string"
            ]
          },
          "photoDataUrl": {
            "type": [
              "null",
              "string"
            ]
          },
          "clearPhoto": {
            "type": [
              "null",
              "boolean"
            ]
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Reference"
    },
    {
      "name": "Identity"
    },
    {
      "name": "Today"
    },
    {
      "name": "Events"
    },
    {
      "name": "Being a guest"
    },
    {
      "name": "Invites"
    }
  ]
}