# PickMe API Reference > Complete API documentation for AI agent and LLM context import. > Version: 1.0.0 | Base URL: https://pickme.solanalink.jp --- ## Overview PickMe is a multilingual travel expert marketplace. Tourists discover verified local guides, book personalized experiences, and connect via real-time messaging. The platform supports English, Chinese (Simplified/Traditional), Japanese, and Korean. ### Key Concepts - **Expert**: A verified local guide with a profile, ratings, and packages - **Tourist**: A user who browses experts and books experiences - **Booking**: A reservation between a tourist and an expert - **Travelogue**: A community blog post about travel experiences - **Package**: A pre-built travel experience offered by an expert --- ## Authentication ### POST /api/v1/auth/login Authenticate with email and password. Returns a JWT bearer token. **Request:** ```json { "email": "user@example.com", "password": "minimum8chars" } ``` **Response (200):** ```json { "token": "eyJhbGciOiJIUzI1NiIs...", "user": { "id": "cuid_...", "email": "user@example.com", "name": "John Doe", "role": "TOURIST", "nickname": "johnd", "avatar": "https://..." } } ``` **Errors:** - 401: Invalid email or password - 400: Validation error (email format, password too short) ### POST /api/v1/auth/register Create a new user account. **Request:** ```json { "email": "user@example.com", "password": "minimum8chars", "name": "John Doe" } ``` **Response (201):** ```json { "token": "eyJhbGciOiJIUzI1NiIs...", "user": { "id": "cuid_...", "email": "user@example.com", "name": "John Doe", "role": "TOURIST" } } ``` **Errors:** - 409: Email already registered - 400: Validation error ### POST /api/v1/auth/google Exchange a Google OAuth ID token for a PickMe JWT. **Request:** ```json { "idToken": "google_id_token_here", "platform": "web" } ``` **Response (200):** Same format as /login ### POST /api/v1/auth/apple Exchange an Apple authorization code for a PickMe JWT. **Request:** ```json { "authorizationCode": "apple_auth_code", "identityToken": "apple_identity_token", "fullName": { "givenName": "John", "familyName": "Doe" } } ``` **Response (200):** Same format as /login ### GET /api/v1/auth/me Get the current authenticated user's profile. **Headers:** `Authorization: Bearer ` **Response (200):** ```json { "id": "cuid_...", "email": "user@example.com", "name": "John Doe", "image": "https://...", "role": "TOURIST", "nickname": "johnd", "avatar": "https://..." } ``` **Errors:** - 401: Missing or invalid token --- ## Using Authentication All authenticated endpoints accept either a JWT or an API key: ``` Authorization: Bearer Authorization: Bearer pk_live_ ``` **JWT Tokens** are issued by /login, /register, /google, or /apple endpoints. Token validity: 30 days. Algorithm: HS256. **API Keys** are created via `POST /api/v1/auth/api-keys` (requires JWT auth). Keys have scoped permissions and configurable rate limits. Key format: `pk_live_<32chars>` (production) or `pk_test_<32chars>` (development). The full key is shown once at creation — store it securely. ### API Key Management #### POST /api/v1/auth/api-keys Create a new API key. Requires JWT authentication. **Request:** ```json { "name": "My Travel Agent Bot", "scopes": ["experts:read", "packages:read", "travelogues:read"], "expiresInDays": 90, "rateLimit": 300 } ``` **Available Scopes:** | Scope | Allows | | ------------------ | ------------------------- | | `experts:read` | List/view expert profiles | | `packages:read` | List/view packages | | `travelogues:read` | List/view travelogues | | `bookings:read` | View own bookings | | `bookings:write` | Create bookings | | `profile:read` | Read own profile | | `profile:write` | Update own profile | **Response (201):** ```json { "data": { "id": "uuid", "key": "pk_live_abc123...", "keyPrefix": "pk_live_abc1", "name": "My Travel Agent Bot", "scopes": ["experts:read", "packages:read"] }, "warning": "Store this key securely. It will not be shown again." } ``` #### GET /api/v1/auth/api-keys List your active API keys (key hash not exposed). #### DELETE /api/v1/auth/api-keys/{id} Revoke an API key. Must own the key. --- ## Pre-Execution Authorization (Two-Step Confirmation) High-risk operations performed via API keys require a two-step confirmation flow: 1. **Initiate**: The original request returns `202 Accepted` with a `pendingActionId` and `confirmUrl` 2. **Confirm**: Call `POST /api/v1/actions/{id}/confirm` within 5 minutes to execute ### High-Risk Actions | Action | Risk Level | | --------------------- | ---------- | | `booking.cancel` | high | | `payment.process` | high | | `account.delete` | high | | `subscription.cancel` | high | | `subscription.create` | high | Medium-risk actions (`booking.create`, `profile.update`) do NOT require confirmation. JWT-authenticated requests are never subject to confirmation — only API key requests. ### GET /api/v1/actions/{id} Check the status of a pending action. **Response (200):** ```json { "data": { "id": "uuid", "action": "booking.cancel", "status": "pending", "expiresAt": "2026-03-24T12:05:00.000Z", "confirmedAt": null, "createdAt": "2026-03-24T12:00:00.000Z" } } ``` Status values: `pending`, `confirmed`, `expired`. ### POST /api/v1/actions/{id}/confirm Confirm a pending action. Only the original actor can confirm. **Response (200):** ```json { "data": { "actionId": "uuid", "action": "booking.cancel", "status": "confirmed", "payload": { "bookingId": "abc123" } } } ``` **Error cases:** - Action expired → `422` (initiate a new request) - Already confirmed → `422` - Wrong actor → `403` ### Audit Trail All authenticated API operations are logged to an audit trail: - Actor identity (user or API key) - Action performed - Resource type and ID - IP address and user agent - Timestamp Audit logs are fire-and-forget (never block requests). --- ## Expert Availability ### GET /api/expert/availability Get an expert's weekly availability schedule. **Headers:** `Authorization: Bearer ` **Query Parameters:** - `expertId` (required): Expert profile ID **Response (200):** ```json { "weeklySchedule": { "monday": [{ "start": "09:00", "end": "17:00" }], "tuesday": [{ "start": "09:00", "end": "17:00" }], "wednesday": [], "thursday": [{ "start": "10:00", "end": "16:00" }], "friday": [{ "start": "09:00", "end": "17:00" }], "saturday": [{ "start": "10:00", "end": "14:00" }], "sunday": [] }, "timezone": "Asia/Shanghai", "blockedDates": ["2026-04-01", "2026-04-02"] } ``` --- ## Subscriptions ### GET /api/subscription Get the current user's messaging subscription status. **Headers:** `Authorization: Bearer ` **Response (200):** ```json { "hasActiveSubscription": true, "subscription": { "id": "cuid_...", "status": "ACTIVE", "plan": "MONTHLY", "currentPeriodEnd": "2026-04-24T00:00:00Z", "cancelAtPeriodEnd": false } } ``` ### PATCH /api/subscription Cancel or reactivate a subscription. **Headers:** `Authorization: Bearer ` **Request:** ```json { "action": "cancel" } ``` Actions: `"cancel"` or `"reactivate"` ### POST /api/subscription/checkout Create a Stripe checkout session for a new subscription. **Headers:** `Authorization: Bearer ` **Request:** ```json { "plan": "MONTHLY", "successUrl": "https://pickme.solanalink.jp/en/dashboard", "cancelUrl": "https://pickme.solanalink.jp/en/dashboard" } ``` **Response (200):** ```json { "checkoutUrl": "https://checkout.stripe.com/..." } ``` --- ## Push Notifications ### POST /api/push/register Register a device for push notifications via Firebase Cloud Messaging. **Headers:** `Authorization: Bearer ` **Request:** ```json { "token": "fcm_device_token_here", "platform": "ios" } ``` Platform: `"ios"`, `"android"`, or `"web"` ### POST /api/push/unregister Remove a push notification registration. **Headers:** `Authorization: Bearer ` **Request:** ```json { "token": "fcm_device_token_here" } ``` --- ## Messaging ### GET /api/agora/token Generate an Agora Chat SDK token for real-time messaging. **Headers:** `Authorization: Bearer ` **Response (200):** ```json { "token": "agora_chat_token_...", "agoraUid": "user_agora_id", "appKey": "agora_app_key" } ``` --- ## User Roles | Role | Description | | ------- | ---------------------------------------------- | | TOURIST | Default role. Can browse, book, and message. | | EXPERT | Verified guide. Can accept bookings, earn. | | ADMIN | Platform administrator. Full access. | --- ## Booking Lifecycle 1. Tourist browses experts or packages 2. Tourist creates a booking (PENDING) 3. Expert confirms the booking (CONFIRMED) 4. Tourist pays via Stripe (PAID) 5. Service is delivered (IN_PROGRESS) 6. Booking completed (COMPLETED) 7. Optional: CANCELLED or REFUNDED Booking statuses: `PENDING`, `CONFIRMED`, `PAID`, `IN_PROGRESS`, `COMPLETED`, `CANCELLED`, `REFUNDED` --- ## Error Response Format All API errors return: ```json { "error": { "code": "ERROR_CODE", "message": "Human-readable description", "details": {} }, "requestId": "req_abc123" } ``` ### Error Codes **Authentication (401):** - `AUTH_REQUIRED` - No authentication token provided - `AUTH_INVALID_CREDENTIALS` - Wrong email or password - `AUTH_SESSION_EXPIRED` - Token has expired - `AUTH_TOKEN_INVALID` - Malformed or tampered token **Authorization (403):** - `FORBIDDEN` - Action not permitted - `ROLE_REQUIRED` - Requires specific role (EXPERT, ADMIN) - `OWNERSHIP_REQUIRED` - Must own the resource **Validation (400):** - `VALIDATION_FAILED` - Input validation errors (check details.fieldErrors) - `INVALID_INPUT` - Malformed request body - `MISSING_REQUIRED_FIELD` - Required field omitted **Resources (404/409):** - `NOT_FOUND` - Resource does not exist - `RESOURCE_EXISTS` - Duplicate (e.g., email already registered) - `RESOURCE_CONFLICT` - Conflicting state **Business Logic (422):** - `BUSINESS_RULE_VIOLATION` - Business rule prevented action - `INVALID_STATE` - Resource in wrong state for operation - `OPERATION_NOT_ALLOWED` - Operation blocked by policy - `QUOTA_EXCEEDED` - Limit reached **External Services (502):** - `EXTERNAL_SERVICE_ERROR` - Third-party service failure - `PAYMENT_ERROR` - Stripe payment issue - `EMAIL_ERROR` - Email delivery failure **System (500):** - `INTERNAL_ERROR` - Unexpected server error - `SERVICE_UNAVAILABLE` - Service temporarily down --- ## Public Read-Only API All public API endpoints require no authentication and are rate-limited at 60 requests/minute per IP. All list endpoints return a paginated envelope: `{ data: [...], meta: { page, pageSize, total, hasMore } }`. All single-resource endpoints return: `{ data: { ... } }`. ### Common Query Parameters | Param | Type | Default | Description | | -------- | ------ | ------- | ---------------------------------------- | | page | number | 1 | Page number (1-indexed) | | pageSize | number | 20 | Items per page (max 100) | | sort | string | varies | Sort field (endpoint-specific) | | order | string | desc | Sort direction: `asc` or `desc` | | locale | string | en | Content language: en, zh, zh-TW, ja, ko | ### GET /api/v1/experts List verified travel experts (paginated). **Filters:** `city`, `language`, `specialty`, `country` (CountryCode: CN, JP, KR, TW, HK) **Sort fields:** `rating` (default), `reviewCount`, `createdAt` **Response (200):** ```json { "data": [ { "handle": "sarah-beijing", "name": "Sarah", "avatar": "https://...", "bio": "Local Beijing guide...", "languages": ["en", "zh"], "specialties": ["food_tour", "culture"], "location": "Beijing", "countryCode": "CN", "currency": "USD", "profileImage": "https://...", "hourlyRate": 50, "rating": 4.8, "reviewCount": 42 } ], "meta": { "page": 1, "pageSize": 20, "total": 150, "hasMore": true } } ``` ### GET /api/v1/experts/{handle} Get a single expert's full profile. **Response (200):** ```json { "data": { "handle": "sarah-beijing", "name": "Sarah", "bio": "...", "languages": ["en", "zh"], "specialties": ["food_tour"], "location": "Beijing", "countryCode": "CN", "currency": "USD", "profileImage": "https://...", "coverImage": "https://...", "hourlyRate": 50, "rating": 4.8, "reviewCount": 42, "socialLinks": { "instagram": "..." }, "messagingEnabled": true, "weeklySchedule": { "1": [{"start":"09:00","end":"17:00"}] }, "timezone": "Asia/Shanghai", "verifiedAt": "2025-01-15T00:00:00Z" } } ``` **Errors:** 404 if expert not found or not verified ### GET /api/v1/experts/{handle}/packages List packages associated with an expert (via bookings). Paginated. **Errors:** 404 if expert not found ### GET /api/v1/experts/{handle}/reviews List public reviews for an expert. Paginated. **Sort fields:** `createdAt` (default), `rating` **Response item:** ```json { "id": "uuid", "rating": 5, "comment": "Amazing experience!", "createdAt": "2025-03-01T00:00:00Z", "author": { "name": "John", "avatar": "https://..." } } ``` **Errors:** 404 if expert not found ### GET /api/v1/packages List all active travel packages (paginated). **Filters:** `category` (e.g., city_tour, food_tour), `country` (CountryCode) **Sort fields:** `createdAt` (default), `basePrice`, `duration` **Response item:** ```json { "id": "uuid", "title": "Beijing Food Tour", "description": "...", "category": "food_tour", "countryCode": "CN", "basePrice": 120, "currency": "USD", "duration": 4, "maxParticipants": 6, "images": ["https://..."], "highlights": ["Visit local markets", "Try street food"], "included": ["Lunch", "Transport"], "meetingPoint": "Wangfujing Station" } ``` ### GET /api/v1/packages/{id} Get a single package by ID. Includes `excluded` items in addition to list fields. **Errors:** 404 if package not found or inactive ### GET /api/v1/travelogues List published travelogues (paginated). **Filters:** `category` (food, adventure, culture, nature) **Sort fields:** `hotScore` (default), `score`, `viewCount`, `createdAt`, `publishedAt` **Response item:** ```json { "slug": "best-ramen-in-tokyo", "title": "Best Ramen in Tokyo", "excerpt": "A guide to...", "coverImage": "https://...", "category": "food", "locationTags": ["Tokyo", "Shinjuku"], "score": 42, "viewCount": 1200, "commentCount": 15, "publishedAt": "2025-02-10T00:00:00Z", "author": { "handle": "tokyo-foodie", "name": "Yuki", "avatar": "https://..." } } ``` ### GET /api/v1/travelogues/{slug} Get a single travelogue by slug. Includes full `content` (HTML), `images` array, and `createdAt`/`updatedAt`. **Errors:** 404 if travelogue not found or not published ### GET /api/v1/cities List distinct cities where verified experts are located. No pagination. **Response (200):** ```json { "data": [ { "name": "Beijing", "countryCode": "CN" }, { "name": "Tokyo", "countryCode": "JP" } ] } ``` ### GET /api/v1/specialties List distinct specialties offered by verified experts. No pagination. **Response (200):** ```json { "data": ["city_tour", "culture", "food_tour", "nature", "shopping"] } ``` --- ## Public Pages & Content ### Expert Profiles URL pattern: `/{locale}/{expert-handle}` (e.g., `/en/sarah-beijing`) Expert profile data includes: - Name, bio (English + Chinese), profile image - Languages spoken, specialties, city - Hourly rate, currency - Rating (1-5), review count - Available packages - Availability schedule ### Travelogues URL pattern: `/{locale}/travelogues/{slug}` Travelogue data includes: - Title, excerpt, full content (English + Chinese) - Author (expert profile) - Cover image, category, location tags - Vote score (upvotes/downvotes), comment count - Published date, last updated ### Packages URL pattern: `/{locale}/packages/{id}` Package data includes: - Title, description (English + Chinese) - Price, currency, duration - Max participants, meeting point - Highlights, images - Expert who offers it --- ## Supported Locales | Code | Language | URL prefix | | ----- | -------------------- | ---------- | | en | English | /en/ | | zh | Chinese (Simplified) | /zh/ | | zh-TW | Chinese (Traditional)| /zh-TW/ | | ja | Japanese | /ja/ | | ko | Korean | /ko/ | All public pages and API responses support bilingual content. Database stores both English (`field`) and Chinese (`fieldZh`) values. --- ## Schema.org Structured Data All public pages include JSON-LD structured data: - **Expert profiles**: `Person` with `AggregateRating` and `ReserveAction` - **Travelogue articles**: `Article` with author, dates, publisher - **Home page**: `Organization` + `LocalBusiness` (TravelAgency) - **Navigation**: `BreadcrumbList` on all detail pages --- ## OpenAPI Specification A machine-readable OpenAPI 3.1 specification is available at: ``` GET /api/openapi.json ``` This includes all public and authenticated endpoints with: - Unique `operationId` for each endpoint (e.g., `listExperts`, `loginWithCredentials`) - Request/response schemas with types, formats, and examples - Authentication requirements (Bearer JWT) - Rate limit tiers per endpoint category - Error response schema with self-healing metadata Use this spec for automated API client generation, agent tool registration, and semantic routing in AI agent frameworks. --- ## Technical Details - **Framework**: Next.js 16 (App Router) - **Database**: MySQL 8 via Prisma ORM - **Payments**: Stripe - **Real-time messaging**: Agora Chat SDK - **Push notifications**: Firebase Cloud Messaging - **File storage**: Huawei Cloud OBS - **PWA**: Full Progressive Web App with offline support