{"openapi":"3.1.0","info":{"title":"Trusted Marketplace API","description":"REST API for the Trusted Services Marketplace. AI-powered search, geo-aware results, feedback intelligence. See docs/api-specification.md for the full integration guide.","version":"0.5.0","contact":{"name":"Trusted Marketplace","url":"https://trustedmarket.io"}},"servers":[{"url":"/api/v1","description":"REST API v1"}],"paths":{"/search":{"get":{"operationId":"searchListings","summary":"Search listings with structured filters","description":"Filter listings by category, location, price, rating, distance. Used for category browsing and manual filter searches.","tags":["Search"],"parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Text search query"},{"name":"category","in":"query","schema":{"type":"string"},"description":"Category slug (e.g. skilled-trades, cleaning, pet-services)"},{"name":"location","in":"query","schema":{"type":"string"},"description":"Location text filter (e.g. Toronto, ON)"},{"name":"priceMin","in":"query","schema":{"type":"number"},"description":"Minimum price (CAD)"},{"name":"priceMax","in":"query","schema":{"type":"number"},"description":"Maximum price (CAD)"},{"name":"pricingModel","in":"query","schema":{"type":"string","enum":["FIXED","HOURLY","QUOTE"]}},{"name":"minRating","in":"query","schema":{"type":"number","minimum":0,"maximum":5},"description":"Minimum provider rating"},{"name":"maxRating","in":"query","schema":{"type":"number","minimum":0,"maximum":5},"description":"Maximum provider rating (rare — for budget-hunting / problem detection)"},{"name":"minReviews","in":"query","schema":{"type":"number"},"description":"Minimum review count"},{"name":"providerName","in":"query","schema":{"type":"string"},"description":"Filter by provider display name (substring + trigram fuzzy match)"},{"name":"recentlyActive","in":"query","schema":{"type":"boolean"},"description":"Only show providers active recently"},{"name":"hasImages","in":"query","schema":{"type":"boolean"},"description":"Only show listings with at least one image"},{"name":"minImages","in":"query","schema":{"type":"integer"},"description":"Minimum number of images on the listing"},{"name":"hasReviews","in":"query","schema":{"type":"boolean"},"description":"Only show providers with at least one review"},{"name":"maxDistanceKm","in":"query","schema":{"type":"number"},"description":"Maximum distance in km"},{"name":"userLat","in":"query","schema":{"type":"number"},"description":"User latitude for distance calculation"},{"name":"userLng","in":"query","schema":{"type":"number"},"description":"User longitude for distance calculation"},{"name":"sort","in":"query","schema":{"type":"string","enum":["newest","price_asc","price_desc","rating","distance","relevance"]}},{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":12,"maximum":50}}],"responses":{"200":{"description":"Search results with pagination","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResults"}}}}}}},"/search/smart":{"get":{"operationId":"smartSearch","summary":"AI-powered natural language search","description":"Send a natural language query. The system extracts intent, applies constraints, and ranks results using Claude AI. Handles typos, informal language, and voice input.","tags":["Search"],"parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":500},"description":"Natural language search query (e.g. 'stove broke', 'dog walker under $25/hr')"},{"name":"userLat","in":"query","schema":{"type":"number"},"description":"User latitude"},{"name":"userLng","in":"query","schema":{"type":"number"},"description":"User longitude"},{"name":"userLocation","in":"query","schema":{"type":"string"},"description":"User location label (e.g. 'Toronto, ON')"}],"responses":{"200":{"description":"AI-ranked results with interpretation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmartSearchResults"}}}}}}},"/listings/{id}":{"get":{"operationId":"getListingById","summary":"Get listing details by ID","tags":["Listings"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Listing details with reviews and provider info"},"404":{"description":"Listing not found"}}},"patch":{"operationId":"updateListing","summary":"Update an existing listing","tags":["Listings"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateListingInput"}}}},"responses":{"200":{"description":"Listing updated"},"401":{"description":"Not authenticated"},"403":{"description":"Not the listing owner"},"404":{"description":"Listing not found"}}}},"/listings":{"post":{"operationId":"createListing","summary":"Create a new service listing","tags":["Listings"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateListingInput"}}}},"responses":{"201":{"description":"Listing created"},"401":{"description":"Not authenticated"}}}},"/listings/{id}/toggle":{"post":{"operationId":"toggleListingStatus","summary":"Toggle listing between ACTIVE and ARCHIVED","tags":["Listings"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Status toggled"},"401":{"description":"Not authenticated"},"403":{"description":"Not the listing owner"}}}},"/images/upload":{"post":{"operationId":"uploadImage","summary":"Upload an image for a profile, listing, or portfolio","tags":["Images"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"Image file (JPEG, PNG, WebP, GIF)"},"bucket":{"type":"string","enum":["profiles","listings","portfolios"],"description":"Storage bucket"},"entityId":{"type":"string","description":"Optional: listing ID or profile ID to associate the image with"}},"required":["file","bucket"]}}}},"responses":{"201":{"description":"Image uploaded successfully. Returns URL and key."},"400":{"description":"Invalid file type, size, or missing fields"},"401":{"description":"Not authenticated"}}}},"/categories":{"get":{"operationId":"getCategories","summary":"Get all service categories with listing counts","tags":["Categories"],"responses":{"200":{"description":"List of categories","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Category"}}}}}}}},"/providers/{id}":{"get":{"operationId":"getProviderById","summary":"Get provider profile with services and reviews","tags":["Providers"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Provider profile with services grouped by category"},"404":{"description":"Provider not found"}}}},"/providers/{id}/reviews":{"get":{"operationId":"getProviderReviews","summary":"Get paginated reviews for a provider","tags":["Providers"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":10,"maximum":50}}],"responses":{"200":{"description":"Paginated reviews"}}}},"/users/profile":{"get":{"operationId":"getCurrentUserProfile","summary":"Get the current authenticated user's profile","tags":["Users"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"User profile with reviews, bookings, provider info"},"401":{"description":"Not authenticated"}}}},"/search/feed":{"get":{"operationId":"searchFeed","summary":"Search with embedded reviews per listing","description":"Same filters as /search but each listing includes its 2 most recent reviews. Max 20 results. Ideal for social-feed style UIs.","tags":["Search"],"parameters":[{"name":"q","in":"query","schema":{"type":"string"}},{"name":"category","in":"query","schema":{"type":"string"}},{"name":"sort","in":"query","schema":{"type":"string"}},{"name":"userLat","in":"query","schema":{"type":"number"}},{"name":"userLng","in":"query","schema":{"type":"number"}},{"name":"limit","in":"query","schema":{"type":"integer","default":10,"maximum":20}},{"name":"page","in":"query","schema":{"type":"integer","default":1}}],"responses":{"200":{"description":"Listings with embedded reviews and pagination"}}}},"/geocode":{"get":{"operationId":"geocode","summary":"Forward, reverse, or suggest geocoding","description":"Three modes: forward (city name → coords), reverse (coords → label), suggest (autocomplete). Powered by AWS Location Service.","tags":["Geo"],"parameters":[{"name":"q","in":"query","schema":{"type":"string","minLength":2},"description":"Location query (forward/suggest mode)"},{"name":"lat","in":"query","schema":{"type":"number"},"description":"Latitude (reverse mode)"},{"name":"lng","in":"query","schema":{"type":"number"},"description":"Longitude (reverse mode)"},{"name":"mode","in":"query","schema":{"type":"string","enum":["reverse","suggest"]},"description":"Mode: omit for forward, 'reverse' for coords→label, 'suggest' for autocomplete"}],"responses":{"200":{"description":"Forward/reverse: { label, lat, lng }. Suggest: { suggestions: [{ label, lat, lng }] }"},"400":{"description":"Query too short or missing params"}}}},"/feedback":{"post":{"operationId":"submitFeedback","summary":"Submit bug report, feature request, or feedback","description":"Saves to database first (instant confirmation), creates GitHub issue async. Detects duplicates by title/description similarity.","tags":["Feedback"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","enum":["bug","feature","feedback"]},"title":{"type":"string","minLength":1},"description":{"type":"string","minLength":1},"screenshots":{"type":"array","items":{"type":"string","format":"uri"}},"context":{"type":"object","description":"pageUrl, searchQuery, browserUA, appVersion"},"markDuplicateOf":{"type":"string","description":"Link as duplicate of existing feedback ID"}},"required":["type","title","description"]}}}},"responses":{"200":{"description":"{ status: 'duplicate', matches: [...] } — similar items found"},"201":{"description":"{ status: 'created', feedbackId } or { status: 'linked', feedbackId, duplicateOf }"},"400":{"description":"Missing title or description"},"401":{"description":"Not authenticated"}}}},"/feedback/search":{"get":{"operationId":"searchFeedback","summary":"Search existing feedback items","description":"Search by title and description. Returns matching items with dupe count. No auth required.","tags":["Feedback"],"parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":3},"description":"Search query"},{"name":"type","in":"query","schema":{"type":"string","enum":["bug","feature","feedback"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":5,"maximum":10}}],"responses":{"200":{"description":"[{ id, title, type, status, createdAt, dupeCount }]"}}}},"/version":{"get":{"operationId":"getVersion","summary":"App version and commit hash","tags":["System"],"responses":{"200":{"description":"{ version, commit }"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Clerk session token. Mobile: use Clerk SDK to get the token. Dev: use mock-login cookie as fallback."}},"schemas":{"SearchResults":{"type":"object","properties":{"listings":{"type":"array","items":{"$ref":"#/components/schemas/ListingSummary"}},"pagination":{"type":"object","properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"totalPages":{"type":"integer"}}}}},"SmartSearchResults":{"type":"object","properties":{"listings":{"type":"array","items":{"$ref":"#/components/schemas/ListingSummary"}},"interpretation":{"type":"string","description":"Human-readable summary of what was understood"},"searchRadiusKm":{"type":"number","nullable":true},"locationLabel":{"type":"string","nullable":true}}},"ListingSummary":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"price":{"type":"number","nullable":true},"pricingModel":{"type":"string","enum":["FIXED","HOURLY","QUOTE"]},"serviceArea":{"type":"string","nullable":true},"distanceKm":{"type":"number","description":"Distance from user in km (if user has coordinates)"},"category":{"type":"object","properties":{"name":{"type":"string"},"slug":{"type":"string"}}},"provider":{"type":"object","properties":{"id":{"type":"string"},"displayName":{"type":"string"},"avgRating":{"type":"number"},"reviewCount":{"type":"integer"}}}}},"Category":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"icon":{"type":"string","nullable":true},"_count":{"type":"object","properties":{"listings":{"type":"integer"}}}}},"CreateListingInput":{"type":"object","required":["providerId","title","description","categoryId","pricingModel","type"],"properties":{"providerId":{"type":"string"},"type":{"type":"string","enum":["SERVICE","ITEM"],"description":"Listing type — service offering or physical item for sale"},"title":{"type":"string","minLength":3,"maxLength":200},"description":{"type":"string","minLength":10,"maxLength":2000},"categoryId":{"type":"string"},"pricingModel":{"type":"string","enum":["FIXED","HOURLY","QUOTE"]},"price":{"type":"number","minimum":0,"nullable":true},"estimatedDuration":{"type":"number","minimum":0,"nullable":true,"description":"SERVICE only — estimated minutes"},"serviceArea":{"type":"string","nullable":true,"description":"SERVICE only — display label for the service area"},"latitude":{"type":"number","nullable":true,"description":"SERVICE only"},"longitude":{"type":"number","nullable":true,"description":"SERVICE only"},"serviceRadius":{"type":"number","nullable":true,"description":"SERVICE only — service radius in km"},"stockQuantity":{"type":"integer","nullable":true,"description":"ITEM only — available stock"},"shippingMethod":{"type":"string","nullable":true,"description":"ITEM only — shipping method label"},"images":{"type":"array","items":{"type":"string","format":"uri"},"description":"Image URLs"},"thumbnailUrl":{"type":"string","nullable":true,"format":"uri","description":"Cover/thumbnail image URL (defaults to images[0])"}}},"UpdateListingInput":{"type":"object","description":"All fields optional — supply only the ones being updated.","properties":{"title":{"type":"string","minLength":3,"maxLength":200},"description":{"type":"string","minLength":10,"maxLength":2000},"categoryId":{"type":"string"},"pricingModel":{"type":"string","enum":["FIXED","HOURLY","QUOTE"]},"price":{"type":"number","minimum":0,"nullable":true},"estimatedDuration":{"type":"number","minimum":0,"nullable":true},"serviceArea":{"type":"string","nullable":true},"latitude":{"type":"number","nullable":true},"longitude":{"type":"number","nullable":true},"serviceRadius":{"type":"number","nullable":true},"stockQuantity":{"type":"integer","nullable":true},"shippingMethod":{"type":"string","nullable":true},"images":{"type":"array","items":{"type":"string","format":"uri"}},"thumbnailUrl":{"type":"string","nullable":true,"format":"uri"}}}}}}