{"openapi":"3.1.0","info":{"title":"PickMe API (Demo)","version":"1.0.0","description":"Demo API of PickMe, a development showcase by SolanaLink Co., Ltd. — not an operating marketplace. Every expert, package and review is sample data, and bookings made through it are not real: no guide is contacted and no payment takes place. Browse local guides, make demo bookings, and interact with a multilingual community.","contact":{"email":"support@solanalink.jp"},"license":{"name":"Proprietary"}},"servers":[{"url":"https://pickme.solanalink.jp","description":"Demo"}],"tags":[{"name":"Public","description":"Unauthenticated read-only endpoints (60 req/min per IP)"},{"name":"Auth","description":"Authentication endpoints (10 req/15min per IP)"},{"name":"Bookings","description":"Your own bookings. API keys need bookings:read to look and bookings:write to act; cancelling additionally goes through a confirmation."},{"name":"Conversations","description":"Messaging with an expert. User tokens only — no API key scope covers writing to another person."},{"name":"Profile","description":"Your own account (profile:read / profile:write)"},{"name":"Push","description":"Push notification token management"},{"name":"Messaging","description":"Real-time messaging credentials"},{"name":"Expert","description":"Expert-specific endpoints"},{"name":"Actions","description":"Two-step confirmation for high-risk API key operations"}],"paths":{"/api/v1/experts":{"get":{"operationId":"listExperts","summary":"List verified experts","tags":["Public"],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1,"minimum":1}},{"name":"pageSize","in":"query","schema":{"type":"integer","default":20,"minimum":1,"maximum":100}},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"locale","in":"query","schema":{"type":"string","enum":["en","zh","zh-TW","ja","ko"],"default":"en"},"description":"Content language preference"},{"name":"sort","in":"query","schema":{"type":"string","enum":["rating","reviewCount","createdAt"],"default":"rating"}},{"name":"city","in":"query","schema":{"type":"string"},"description":"Filter by city name (partial match)"},{"name":"language","in":"query","schema":{"type":"string"},"description":"Filter by language code"},{"name":"specialty","in":"query","schema":{"type":"string"},"description":"Filter by specialty tag"},{"name":"country","in":"query","schema":{"type":"string","enum":["CN","JP","KR","TW","HK"]}}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ExpertSummary"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/experts/{handle}":{"get":{"operationId":"getExpertByHandle","summary":"Get expert profile by handle","tags":["Public"],"parameters":[{"name":"handle","in":"path","required":true,"schema":{"type":"string"}},{"name":"locale","in":"query","schema":{"type":"string","enum":["en","zh","zh-TW","ja","ko"],"default":"en"},"description":"Content language preference"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ExpertDetail"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/experts/{handle}/packages":{"get":{"operationId":"getExpertPackages","summary":"List an expert's packages","tags":["Public"],"parameters":[{"name":"handle","in":"path","required":true,"schema":{"type":"string"}},{"name":"page","in":"query","schema":{"type":"integer","default":1,"minimum":1}},{"name":"pageSize","in":"query","schema":{"type":"integer","default":20,"minimum":1,"maximum":100}},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"locale","in":"query","schema":{"type":"string","enum":["en","zh","zh-TW","ja","ko"],"default":"en"},"description":"Content language preference"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PackageSummary"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/experts/{handle}/reviews":{"get":{"operationId":"getExpertReviews","summary":"List an expert's public reviews","tags":["Public"],"parameters":[{"name":"handle","in":"path","required":true,"schema":{"type":"string"}},{"name":"page","in":"query","schema":{"type":"integer","default":1,"minimum":1}},{"name":"pageSize","in":"query","schema":{"type":"integer","default":20,"minimum":1,"maximum":100}},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"locale","in":"query","schema":{"type":"string","enum":["en","zh","zh-TW","ja","ko"],"default":"en"},"description":"Content language preference"},{"name":"sort","in":"query","schema":{"type":"string","enum":["createdAt","rating"],"default":"createdAt"}}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Review"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/packages":{"get":{"operationId":"listPackages","summary":"List active travel packages","tags":["Public"],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1,"minimum":1}},{"name":"pageSize","in":"query","schema":{"type":"integer","default":20,"minimum":1,"maximum":100}},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"locale","in":"query","schema":{"type":"string","enum":["en","zh","zh-TW","ja","ko"],"default":"en"},"description":"Content language preference"},{"name":"sort","in":"query","schema":{"type":"string","enum":["basePrice","duration","createdAt"],"default":"createdAt"}},{"name":"category","in":"query","schema":{"type":"string"},"description":"e.g. city_tour, food_tour, shopping"},{"name":"country","in":"query","schema":{"type":"string","enum":["CN","JP","KR","TW","HK"]}}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PackageSummary"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/packages/{id}":{"get":{"operationId":"getPackageById","summary":"Get package details","tags":["Public"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"locale","in":"query","schema":{"type":"string","enum":["en","zh","zh-TW","ja","ko"],"default":"en"},"description":"Content language preference"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PackageDetail"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/travelogues":{"get":{"operationId":"listTravelogues","summary":"List published travelogues","tags":["Public"],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1,"minimum":1}},{"name":"pageSize","in":"query","schema":{"type":"integer","default":20,"minimum":1,"maximum":100}},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"locale","in":"query","schema":{"type":"string","enum":["en","zh","zh-TW","ja","ko"],"default":"en"},"description":"Content language preference"},{"name":"sort","in":"query","schema":{"type":"string","enum":["hotScore","score","viewCount","createdAt","publishedAt"],"default":"hotScore"}},{"name":"category","in":"query","schema":{"type":"string"},"description":"e.g. food, adventure, culture, nature"}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TravelogueSummary"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/travelogues/{slug}":{"get":{"operationId":"getTravelogueBySlug","summary":"Get travelogue by slug","tags":["Public"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"locale","in":"query","schema":{"type":"string","enum":["en","zh","zh-TW","ja","ko"],"default":"en"},"description":"Content language preference"}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TravelogueDetail"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cities":{"get":{"operationId":"listCities","summary":"List cities with verified experts","tags":["Public"],"responses":{"200":{"description":"List of cities","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/City"}}}}}}}}}},"/api/v1/specialties":{"get":{"operationId":"listSpecialties","summary":"List available expert specialties","tags":["Public"],"responses":{"200":{"description":"List of specialty strings","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"string"}}}}}}}}}},"/api/v1/auth/login":{"post":{"operationId":"loginWithCredentials","summary":"Authenticate with email and password","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":8}}}}}},"responses":{"200":{"description":"JWT token and user profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auth/register":{"post":{"operationId":"registerUser","summary":"Create a new user account","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password","name","role"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":8},"name":{"type":"string","minLength":2},"role":{"type":"string","enum":["TOURIST","EXPERT"]}}}}}},"responses":{"201":{"description":"Account created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"409":{"description":"Email already registered"}}}},"/api/v1/auth/google":{"post":{"operationId":"loginWithGoogle","summary":"Exchange Google OAuth ID token for JWT","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["idToken"],"properties":{"idToken":{"type":"string","description":"Google OAuth ID token"}}}}}},"responses":{"200":{"description":"JWT token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auth/apple":{"post":{"operationId":"loginWithApple","summary":"Exchange Apple identity token for JWT","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["identityToken"],"properties":{"identityToken":{"type":"string"},"fullName":{"type":"object","nullable":true,"properties":{"givenName":{"type":"string","nullable":true},"familyName":{"type":"string","nullable":true}}}}}}}},"responses":{"200":{"description":"JWT token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auth/me":{"get":{"operationId":"getCurrentUser","summary":"Get authenticated user profile","tags":["Auth"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"User profile","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/UserResponse"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auth/session-exchange":{"post":{"operationId":"exchangeSessionToken","summary":"Exchange mobile JWT for web session cookie","tags":["Auth"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Session cookie set","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auth/api-keys":{"post":{"operationId":"createApiKey","summary":"Create a new API key (requires JWT auth)","tags":["Auth"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","scopes"],"properties":{"name":{"type":"string","description":"Descriptive name for the key","example":"My Travel Agent Bot"},"scopes":{"type":"array","items":{"type":"string","enum":["experts:read","packages:read","travelogues:read","bookings:read","bookings:write","profile:read","profile:write"]},"description":"Permission scopes for this key"},"expiresInDays":{"type":"integer","minimum":1,"maximum":365,"description":"Key expiry in days (optional, no expiry if omitted)"},"rateLimit":{"type":"integer","minimum":1,"maximum":1000,"default":300,"description":"Max requests/min for this key"}}}}}},"responses":{"201":{"description":"API key created (key shown once)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"key":{"type":"string","description":"Full API key — store securely, shown once"},"keyPrefix":{"type":"string","example":"pk_live_abc1"},"name":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}}}},"warning":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"listApiKeys","summary":"List your active API keys","tags":["Auth"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List of API keys (hash not included)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"keyPrefix":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"rateLimit":{"type":"integer"},"lastUsedAt":{"type":"string","format":"date-time","nullable":true},"expiresAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}}}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auth/api-keys/{id}":{"delete":{"operationId":"revokeApiKey","summary":"Revoke an API key","tags":["Auth"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"API key revoked"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/bookings":{"get":{"operationId":"listBookings","summary":"List your bookings","description":"Bookings where you are the traveller, plus the ones booked with you if you are an expert. An API key needs the bookings:read scope.","tags":["Bookings"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1,"minimum":1}},{"name":"pageSize","in":"query","schema":{"type":"integer","default":20,"minimum":1,"maximum":100}},{"name":"sort","in":"query","schema":{"type":"string","enum":["createdAt","scheduledDate"],"default":"createdAt"}},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/BookingStatus"}}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/BookingSummary"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden — not yours, or the API key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createBooking","summary":"Request a booking","description":"Creates a PENDING booking for the expert to accept. Give expertId, packageId, or both. Payment is arranged offline between the two of you — the platform handles none of it. An API key needs the bookings:write scope.","tags":["Bookings"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["scheduledDate"],"properties":{"expertId":{"type":"string","format":"uuid"},"packageId":{"type":"string","format":"uuid"},"scheduledDate":{"type":"string","format":"date-time"},"scheduledEndDate":{"type":"string","format":"date-time"},"participants":{"type":"integer","minimum":1,"maximum":50,"default":1},"customRequest":{"type":"string","maxLength":2000},"meetingLocation":{"type":"string","maxLength":500}}}}}},"responses":{"201":{"description":"Booking requested","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"$ref":"#/components/schemas/BookingStatus"},"scheduledDate":{"type":"string","format":"date-time"}}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden — not yours, or the API key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Expert is not verified, or the package is not active","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/bookings/{id}":{"get":{"operationId":"getBooking","summary":"Get one of your bookings","description":"Readable by the traveller, the expert, and an admin. An API key needs the bookings:read scope.","tags":["Bookings"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/BookingDetail"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden — not yours, or the API key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/bookings/{id}/cancel":{"post":{"operationId":"cancelBooking","summary":"Cancel a booking","description":"The traveller, the expert or an admin may cancel, up until the booking is COMPLETED. Classified high risk, so an API key does not cancel directly: it receives a pending action (202) and the cancellation happens when that is confirmed.","tags":["Bookings"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Booking cancelled","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"$ref":"#/components/schemas/BookingStatus"},"cancelledAt":{"type":"string","format":"date-time"}}}}}}}},"202":{"description":"Confirmation required (API key)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PendingActionAccepted"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden — not yours, or the API key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Already cancelled, or already completed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/bookings/{id}/review":{"post":{"operationId":"reviewBooking","summary":"Review a completed booking","description":"One review per booking, written by the traveller who made it, only once the booking is COMPLETED. Posting it moves the expert rating in the same call. An API key needs the bookings:write scope.","tags":["Bookings"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["rating"],"properties":{"rating":{"type":"integer","minimum":1,"maximum":5},"comment":{"type":"string","maxLength":2000}}}}}},"responses":{"201":{"description":"Review written, expert rating recomputed","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"rating":{"type":"integer"},"comment":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"expert":{"type":"object","properties":{"rating":{"type":"number"},"reviewCount":{"type":"integer"}}}}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden — not yours, or the API key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Booking is not completed, or already has a review","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations":{"get":{"operationId":"listConversations","summary":"List your conversations","description":"Newest activity first, with the unread count from the other party. User tokens only — API keys cannot reach messaging.","tags":["Conversations"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Conversations","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ConversationSummary"}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden — not yours, or the API key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createConversation","summary":"Open a conversation with an expert","description":"Returns the existing conversation if there is one (200), otherwise creates it (201). Fails if the expert has messaging switched off. User tokens only.","tags":["Conversations"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["expertHandle"],"properties":{"expertHandle":{"type":"string","example":"sarah-beijing"}}}}}},"responses":{"200":{"description":"Existing conversation"},"201":{"description":"Conversation created"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden — not yours, or the API key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Expert has messaging disabled, or you are that expert","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations/{id}/messages":{"get":{"operationId":"listMessages","summary":"Read a conversation","description":"Cursor-paginated, oldest first within the page. Pass the returned nextCursor to walk further back. User tokens only.","tags":["Conversations"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Message id to continue from"},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":100}}],"responses":{"200":{"description":"Messages","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"messages":{"type":"array","items":{"$ref":"#/components/schemas/Message"}},"hasMore":{"type":"boolean"},"nextCursor":{"type":"string","nullable":true}}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden — not yours, or the API key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"sendMessage","summary":"Send a message","description":"Notifies the recipient, and may draw an AI reply if the expert has auto-reply configured. Pass agoraMessageId to make a retry idempotent against the realtime channel. User tokens only.","tags":["Conversations"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["content"],"properties":{"content":{"type":"string","minLength":1,"maxLength":5000},"messageType":{"type":"string","enum":["TEXT","IMAGE"],"default":"TEXT"},"agoraMessageId":{"type":"string","description":"Agora Chat message id, if the message was sent there first"}}}}}},"responses":{"200":{"description":"The agoraMessageId was already recorded — the existing message is returned","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Message"}}}}}},"201":{"description":"Message sent","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Message"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden — not yours, or the API key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations/{id}/read":{"post":{"operationId":"markConversationRead","summary":"Mark a conversation read","description":"Marks every message from the other party as read. User tokens only.","tags":["Conversations"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Marked","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"markedCount":{"type":"integer"}}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden — not yours, or the API key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations/unread-count":{"get":{"operationId":"getUnreadCount","summary":"Count unread messages","description":"Across every conversation you are part of. User tokens only.","tags":["Conversations"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Unread total","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"unreadCount":{"type":"integer"}}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden — not yours, or the API key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/profile":{"get":{"operationId":"getProfile","summary":"Get your profile","description":"An API key needs the profile:read scope.","tags":["Profile"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Profile"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden — not yours, or the API key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"operationId":"updateProfile","summary":"Update your profile","description":"Only the fields you send are touched. An API key needs the profile:write scope.","tags":["Profile"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":100,"nullable":true},"nickname":{"type":"string","maxLength":50,"nullable":true},"bio":{"type":"string","maxLength":500,"nullable":true},"bioZh":{"type":"string","maxLength":500,"nullable":true,"description":"Chinese bio. There is no Korean or Traditional Chinese column."}}}}}},"responses":{"200":{"description":"Updated profile","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Profile"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden — not yours, or the API key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/profile/avatar":{"post":{"operationId":"uploadAvatar","summary":"Upload an avatar","description":"multipart/form-data with a \"file\" field. JPEG, PNG or WebP, up to 5MB. An API key needs the profile:write scope.","tags":["Profile"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"Stored","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"url":{"type":"string","format":"uri"}}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden — not yours, or the API key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/profile/delete":{"get":{"operationId":"checkAccountDeletion","summary":"Check whether the account can be closed","description":"An account with a booking still to happen cannot be closed — the other party is relying on it. An API key needs the profile:write scope.","tags":["Profile"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"responses":{"200":{"description":"Eligibility","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"canDelete":{"type":"boolean"},"blockers":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["active_bookings"]},"count":{"type":"integer"},"message":{"type":"string"}}}}}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden — not yours, or the API key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"deleteAccount","summary":"Close your account","description":"Anonymises the account and removes every way back into it. Bookings, messages and travelogues stay, attributed to \"Deleted User\", because other people depend on them. Send confirmation \"DELETE\". Classified high risk, so an API key receives a pending action (202) and the deletion happens on confirmation.","tags":["Profile"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["confirmation"],"properties":{"confirmation":{"type":"string","enum":["DELETE"]}}}}}},"responses":{"200":{"description":"Account closed"},"202":{"description":"Confirmation required (API key)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PendingActionAccepted"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden — not yours, or the API key lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Blocked by an active booking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/push/register":{"post":{"operationId":"registerPushToken","summary":"Register FCM push notification token","tags":["Push"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string","description":"FCM device token"},"device":{"type":"string","description":"Human-readable device description, e.g. \"Pixel 8\""},"platform":{"type":"string","enum":["WEB","ANDROID","IOS"],"description":"Client platform. Omit and it is derived from how the request authenticated: a bearer token means one of the native apps, a session cookie means a browser."},"appVersion":{"type":"string","example":"1.0.1","description":"Marketing version of the app. Omit on web."},"buildNumber":{"type":"integer","description":"Android versionCode or iOS CFBundleVersion."},"deviceLocale":{"type":"string","example":"ja","description":"Device language tag. Used to pick the language of a notification, ahead of the account setting."},"timezone":{"type":"string","example":"Asia/Tokyo","description":"IANA timezone. Recorded so quiet hours can be judged against the device rather than the server."}}}}}},"responses":{"200":{"description":"Token registered"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/push/unregister":{"post":{"operationId":"unregisterPushToken","summary":"Remove FCM push notification token","tags":["Push"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string"}}}}}},"responses":{"200":{"description":"Token removed"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/agora/token":{"get":{"operationId":"getAgoraToken","summary":"Get Agora Chat SDK credentials","tags":["Messaging"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Agora credentials","content":{"application/json":{"schema":{"type":"object","properties":{"appId":{"type":"string"},"userId":{"type":"string"},"token":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/expert/availability":{"get":{"operationId":"getExpertAvailability","summary":"Get current expert's availability schedule","tags":["Expert"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Availability settings","content":{"application/json":{"schema":{"type":"object","properties":{"expertId":{"type":"string"},"weeklySchedule":{"type":"object"},"timezone":{"type":"string"},"blockedDates":{"type":"array","items":{"type":"object"}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/actions/{id}":{"get":{"operationId":"getActionStatus","summary":"Check the status of a pending action","description":"Returns the current status (pending, confirmed, expired) of a high-risk action that was flagged for confirmation.","tags":["Actions"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Pending action ID"}],"responses":{"200":{"description":"Action status","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"action":{"type":"string","example":"booking.cancel"},"status":{"type":"string","enum":["pending","confirmed","expired"]},"expiresAt":{"type":"string","format":"date-time"},"confirmedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/actions/{id}/confirm":{"post":{"operationId":"confirmAction","summary":"Confirm a pending high-risk action, and carry it out","description":"Confirms a pending action created when an API key asked for a high-risk operation, and performs it. Only the original actor can confirm — which may be the person approving in the app what their key asked for. Do not re-issue the original request afterwards: it is already done, and repeating it would only ask again. The operation runs under rules re-checked at this moment, so it can still fail; the action stays confirmed either way, and a failure means starting a new request.","tags":["Actions"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Pending action ID"}],"responses":{"200":{"description":"Action confirmed","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"actionId":{"type":"string","format":"uuid"},"action":{"type":"string","example":"booking.cancel"},"status":{"type":"string","enum":["confirmed"]},"payload":{"type":"object","description":"The request as it was made"},"result":{"type":"object","description":"What the operation produced — for booking.cancel, the cancelled booking"}}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Action expired or already confirmed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","example":"NOT_FOUND"},"message":{"type":"string","example":"Resource not found"},"details":{"type":"object"},"doc_url":{"type":"string","format":"uri"},"is_retriable":{"type":"boolean"},"retry_after":{"type":"integer"},"alternative_action":{"type":"string"}},"required":["code","message","doc_url","is_retriable"]},"requestId":{"type":"string"}}},"PaginationMeta":{"type":"object","properties":{"page":{"type":"integer","example":1},"pageSize":{"type":"integer","example":20},"total":{"type":"integer","example":150},"hasMore":{"type":"boolean","example":true}},"required":["page","pageSize","total","hasMore"]},"UserResponse":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":"string","nullable":true},"image":{"type":"string","nullable":true},"nickname":{"type":"string","nullable":true},"avatar":{"type":"string","nullable":true},"role":{"type":"string","enum":["TOURIST","EXPERT","ADMIN"]}}},"TokenResponse":{"type":"object","properties":{"token":{"type":"string","description":"JWT bearer token (30-day expiry, HS256)"},"user":{"$ref":"#/components/schemas/UserResponse"}},"required":["token","user"]},"ExpertSummary":{"type":"object","properties":{"handle":{"type":"string","example":"sarah-beijing"},"name":{"type":"string","nullable":true},"avatar":{"type":"string","nullable":true},"bio":{"type":"string","nullable":true},"languages":{"type":"array","items":{"type":"string"}},"specialties":{"type":"array","items":{"type":"string"}},"location":{"type":"string","nullable":true},"countryCode":{"type":"string","enum":["CN","JP","KR","TW","HK"]},"currency":{"type":"string"},"profileImage":{"type":"string","nullable":true},"hourlyRate":{"type":"number","nullable":true},"rating":{"type":"number","example":4.8},"reviewCount":{"type":"integer"}}},"ExpertDetail":{"allOf":[{"$ref":"#/components/schemas/ExpertSummary"},{"type":"object","properties":{"coverImage":{"type":"string","nullable":true},"socialLinks":{"type":"object"},"messagingEnabled":{"type":"boolean"},"weeklySchedule":{"type":"object"},"timezone":{"type":"string","example":"Asia/Shanghai"},"verifiedAt":{"type":"string","format":"date-time","nullable":true}}}]},"PackageSummary":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"countryCode":{"type":"string","enum":["CN","JP","KR","TW","HK"]},"basePrice":{"type":"number"},"currency":{"type":"string"},"duration":{"type":"integer","description":"Duration in hours"},"maxParticipants":{"type":"integer"},"images":{"type":"array","items":{"type":"string"}},"highlights":{"type":"array","items":{"type":"string"}},"included":{"type":"array","items":{"type":"string"}},"meetingPoint":{"type":"string","nullable":true}}},"PackageDetail":{"allOf":[{"$ref":"#/components/schemas/PackageSummary"},{"type":"object","properties":{"excluded":{"type":"array","items":{"type":"string"}}}}]},"TravelogueSummary":{"type":"object","properties":{"slug":{"type":"string"},"title":{"type":"string"},"excerpt":{"type":"string","nullable":true},"coverImage":{"type":"string","nullable":true},"category":{"type":"string"},"locationTags":{"type":"array","items":{"type":"string"}},"score":{"type":"integer"},"viewCount":{"type":"integer"},"commentCount":{"type":"integer"},"publishedAt":{"type":"string","format":"date-time","nullable":true},"author":{"type":"object","properties":{"handle":{"type":"string"},"name":{"type":"string","nullable":true},"avatar":{"type":"string","nullable":true}}}}},"TravelogueDetail":{"allOf":[{"$ref":"#/components/schemas/TravelogueSummary"},{"type":"object","properties":{"content":{"type":"string","description":"Rich text HTML content"},"images":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}]},"Review":{"type":"object","properties":{"id":{"type":"string"},"rating":{"type":"integer","minimum":1,"maximum":5},"comment":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"author":{"type":"object","properties":{"name":{"type":"string","nullable":true},"avatar":{"type":"string","nullable":true}}}}},"City":{"type":"object","properties":{"name":{"type":"string"},"countryCode":{"type":"string","enum":["CN","JP","KR","TW","HK"]}}},"BookingStatus":{"type":"string","enum":["PENDING","CONFIRMED","IN_PROGRESS","COMPLETED","CANCELLED"],"description":"PENDING until the expert accepts, IN_PROGRESS while it is happening, COMPLETED once it has. Only a COMPLETED booking can be reviewed."},"BookingSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"$ref":"#/components/schemas/BookingStatus"},"scheduledDate":{"type":"string","format":"date-time"},"scheduledEndDate":{"type":"string","format":"date-time","nullable":true},"participants":{"type":"integer"},"touristName":{"type":"string","nullable":true},"expertHandle":{"type":"string","nullable":true},"expertName":{"type":"string","nullable":true},"packageId":{"type":"string","nullable":true},"packageTitle":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"BookingDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"$ref":"#/components/schemas/BookingStatus"},"scheduledDate":{"type":"string","format":"date-time"},"scheduledEndDate":{"type":"string","format":"date-time","nullable":true},"participants":{"type":"integer"},"customRequest":{"type":"string","nullable":true},"meetingLocation":{"type":"string","nullable":true},"tourist":{"type":"object","properties":{"name":{"type":"string","nullable":true},"email":{"type":"string","format":"email","nullable":true},"image":{"type":"string","nullable":true}}},"expert":{"type":"object","nullable":true,"properties":{"handle":{"type":"string"},"name":{"type":"string","nullable":true},"image":{"type":"string","nullable":true}}},"package":{"type":"object","nullable":true,"properties":{"id":{"type":"string"},"title":{"type":"string"},"price":{"type":"number","description":"Agreed offline; the platform takes none of it"},"currency":{"type":"string"},"duration":{"type":"integer","description":"Duration in hours"}}},"confirmedAt":{"type":"string","format":"date-time","nullable":true},"completedAt":{"type":"string","format":"date-time","nullable":true},"cancelledAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"review":{"type":"object","nullable":true,"description":"The review written for this booking, if there is one","properties":{"id":{"type":"string"},"rating":{"type":"integer","minimum":1,"maximum":5},"comment":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"canReview":{"type":"boolean","description":"Whether you, specifically, still have a review to write here: you are the traveller, the booking is COMPLETED, it has an expert, and no review exists yet. The same rules POST /bookings/{id}/review enforces, so you can offer the form without guessing."}}},"ConversationSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"lastMessage":{"type":"string","nullable":true},"lastMessageAt":{"type":"string","format":"date-time","nullable":true},"unreadCount":{"type":"integer","description":"Messages from the other party you have not read"},"expert":{"type":"object","properties":{"handle":{"type":"string"},"name":{"type":"string","nullable":true},"image":{"type":"string","nullable":true}}},"tourist":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","nullable":true},"image":{"type":"string","nullable":true}}},"createdAt":{"type":"string","format":"date-time"}}},"Message":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"conversationId":{"type":"string","format":"uuid"},"senderId":{"type":"string"},"messageType":{"type":"string","enum":["TEXT","IMAGE"]},"content":{"type":"string","nullable":true},"imageUrl":{"type":"string","nullable":true},"imageWidth":{"type":"integer","nullable":true},"imageHeight":{"type":"integer","nullable":true},"isRead":{"type":"boolean"},"isAiGenerated":{"type":"boolean","description":"True when an expert has AI auto-reply enabled and this reply came from it"},"createdAt":{"type":"string","format":"date-time"},"sender":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","nullable":true},"image":{"type":"string","nullable":true}}}}},"Profile":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","nullable":true},"email":{"type":"string","format":"email","nullable":true},"image":{"type":"string","nullable":true},"nickname":{"type":"string","nullable":true},"avatar":{"type":"string","nullable":true},"bio":{"type":"string","nullable":true},"role":{"type":"string","enum":["TOURIST","EXPERT","ADMIN"]},"createdAt":{"type":"string","format":"date-time"},"expert":{"type":"object","nullable":true,"description":"Present only for an expert account","properties":{"handle":{"type":"string"},"specialties":{"type":"array","items":{"type":"string"}},"languages":{"type":"array","items":{"type":"string"}},"hourlyRate":{"type":"number","nullable":true},"location":{"type":"string","nullable":true},"countryCode":{"type":"string","nullable":true},"rating":{"type":"number"},"reviewCount":{"type":"integer"}}}}},"PendingActionAccepted":{"type":"object","description":"Returned instead of doing the work when an API key asks for a high-risk operation. Confirm it to have it carried out.","properties":{"actionId":{"type":"string","format":"uuid"},"confirmUrl":{"type":"string","example":"/api/v1/actions/{id}/confirm"},"expiresIn":{"type":"integer","example":300,"description":"Seconds"},"message":{"type":"string"}}}},"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"JWT token obtained from /api/v1/auth/login, /register, /google, or /apple. Valid for 30 days."},"apiKeyAuth":{"type":"apiKey","in":"header","name":"Authorization","description":"API key with \"Bearer pk_live_...\" or \"Bearer pk_test_...\" prefix. Create via POST /api/v1/auth/api-keys."}}}}