{"openapi":"3.1.0","info":{"title":"Owning.pro API","version":"1.0.0","description":"Classifieds portal API — buy and sell new and second-hand items, any category. All endpoints return JSON; listings are also available as Markdown for machine-readable consumption. \n\n## Authentication\n\nPublic read endpoints (GET) require no authentication. Write operations (POST/PUT/DELETE/PATCH) require authentication via JWT or API key.\n\nUse the `Authorization: Bearer <token>` header. Tokens starting with `own_` are API keys; all others are JWT sessions.\n\n## Rate Limiting\n\n- Unauthenticated: 100 req/min per IP\n- Authenticated: 300 req/min per user/key\n\n## Response Formats\n\nListings support both JSON (`application/json`) and Markdown (`text/markdown`) responses. Use the `.md` suffix on listing URLs or send `Accept: text/markdown` to get Markdown.","contact":{"name":"Owning.pro","url":"https://owning.pro"},"license":{"name":"Proprietary"}},"servers":[{"url":"https://api.owning.pro","description":"Production API"},{"url":"https://owning.pro/api","description":"Production (via web proxy)"}],"tags":[{"name":"Listings","description":"Search, browse, create, and manage listings"},{"name":"Categories","description":"Browse the category tree"},{"name":"Asset Types","description":"Discover structured attribute templates per asset type"},{"name":"Schema","description":"JSON Schema and API schema discovery"},{"name":"Auth","description":"User registration, login, and API key management"},{"name":"Upload","description":"Image upload for listings"},{"name":"Discovery","description":"Agent discovery and well-known endpoints"},{"name":"Stripe","description":"Premium listing payments and Stripe webhook handling"},{"name":"Analytics","description":"Event tracking — public POST /api/track endpoint"},{"name":"Favorites","description":"User favorites — bookmark listings"},{"name":"Saved Searches","description":"User saved searches — save and re-run search queries"},{"name":"Messaging","description":"In-app buyer ↔ seller conversations about listings"},{"name":"User Profiles","description":"Public user profiles, listings, and reviews"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Use `Authorization: Bearer <token>`. Tokens starting with `own_` are API keys; all others are JWT sessions."}},"schemas":{"Listing":{"type":"object","required":["id","slug","title","description","category","condition","type","price","location","images","seller","status","created_at","updated_at","expires_at","views","tags","meta"],"properties":{"id":{"type":"string","description":"ULID identifier","example":"lst_01JX5KQM3HZJTN9P0X3VY6BQR"},"slug":{"type":"string","description":"URL-friendly slug","example":"bavaria-c42-cruiser-2021"},"title":{"type":"string","minLength":5,"maxLength":120,"example":"Bavaria C42 Cruiser (2021)"},"description":{"type":"string","minLength":20,"maxLength":5000,"example":"Excellent family cruiser, well maintained, ready to sail."},"long_description":{"type":"string","nullable":true,"description":"Full description from the source page. Only included in detail endpoint (GET /api/listings/{id}), not in list responses.","example":"This Bavaria C42 is in excellent condition..."},"category":{"$ref":"#/components/schemas/CategoryRef"},"condition":{"type":"string","enum":["new","like_new","good","fair","poor","refurbished"],"example":"good"},"type":{"type":"string","enum":["sale","wanted"],"example":"sale"},"price":{"$ref":"#/components/schemas/Price"},"location":{"$ref":"#/components/schemas/Location"},"images":{"type":"array","maxItems":10,"items":{"$ref":"#/components/schemas/Image"}},"attributes":{"type":"object","description":"Structured attributes specific to the listing's asset type (e.g. boat length, year, engine type). See GET /api/asset-types/{type}.","additionalProperties":true},"seller":{"$ref":"#/components/schemas/Seller"},"status":{"type":"string","enum":["draft","active","paused","sold","expired","flagged"],"example":"active"},"created_at":{"type":"string","format":"date-time","example":"2026-07-10T12:00:00.000Z"},"updated_at":{"type":"string","format":"date-time","example":"2026-07-10T12:00:00.000Z"},"expires_at":{"type":"string","format":"date-time","example":"2026-08-09T12:00:00.000Z"},"views":{"type":"integer","minimum":0,"example":42},"tags":{"type":"array","maxItems":10,"items":{"type":"string","maxLength":50},"example":["sailboat","cruiser","bavaria"]},"meta":{"$ref":"#/components/schemas/ListingMeta"}}},"CategoryRef":{"type":"object","required":["id","name","path"],"properties":{"id":{"type":"string","example":"boats"},"name":{"type":"string","example":"Boats"},"path":{"type":"array","items":{"type":"string"},"description":"Full category path from root to this category","example":["vehicles","boats"]}}},"Price":{"type":"object","required":["amount","currency","negotiable"],"properties":{"amount":{"type":"number","minimum":0,"example":89000},"currency":{"type":"string","length":3,"description":"ISO 4217 currency code","example":"EUR"},"negotiable":{"type":"boolean","example":true}}},"Location":{"type":"object","required":["country","city","shipping"],"properties":{"country":{"type":"string","length":2,"description":"ISO 3166-1 alpha-2 country code","example":"ES"},"city":{"type":"string","example":"Palma de Mallorca"},"postal_code":{"type":"string","example":"07015"},"lat":{"type":"number","example":39.5696},"lng":{"type":"number","example":2.6502},"shipping":{"type":"boolean","example":false}}},"Image":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","example":"https://images.owning.pro/listings/bavaria-c42-1.jpg"},"alt":{"type":"string","example":"Bavaria C42 starboard view"}}},"Seller":{"type":"object","required":["id","name","type","verified","member_since"],"properties":{"id":{"type":"string","example":"usr_01JX5KQM3HZJTN9P0X3VY6BQR"},"name":{"type":"string","example":"Owning Marketplace"},"type":{"type":"string","enum":["agent","human"],"example":"agent"},"verified":{"type":"boolean","example":true},"member_since":{"type":"string","format":"date","example":"2026-07-10"}}},"ListingMeta":{"type":"object","required":["source","language"],"properties":{"source":{"type":"string","enum":["api","web","seed","scraped"],"example":"scraped"},"language":{"type":"string","length":2,"description":"ISO 639-1 language code","example":"en"},"origin":{"$ref":"#/components/schemas/ListingOrigin"}}},"ListingOrigin":{"type":"object","required":["url","normalized_url","site","scraped_at","raw_record"],"properties":{"url":{"type":"string","format":"uri","example":"https://www.boat24.com/en/listing/bavaria-c42"},"normalized_url":{"type":"string","example":"boat24.com/en/listing/bavaria-c42"},"site":{"type":"string","example":"boat24.com"},"scraped_at":{"type":"string","format":"date-time","example":"2026-07-10T20:30:00.000Z"},"raw_record":{"type":"object","additionalProperties":true,"description":"Original scraped record"}}},"Pagination":{"type":"object","required":["page","limit","total","pages"],"properties":{"page":{"type":"integer","minimum":1,"example":1},"limit":{"type":"integer","minimum":1,"example":20},"total":{"type":"integer","minimum":0,"example":712},"pages":{"type":"integer","minimum":0,"example":36}}},"PaginatedListings":{"type":"object","required":["results","pagination"],"properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/Listing"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"warnings":{"type":"array","items":{"type":"string"},"description":"Non-fatal warnings (e.g. unrecognized attribute filter keys)"}}},"CategoryTreeNode":{"type":"object","required":["id","name","slug","count"],"properties":{"id":{"type":"string","example":"vehicles"},"name":{"type":"string","example":"Vehicles"},"slug":{"type":"string","example":"vehicles"},"count":{"type":"integer","minimum":0,"example":712},"children":{"type":"array","items":{"$ref":"#/components/schemas/CategoryTreeNode"}}}},"CategoryTreeResponse":{"type":"object","required":["categories"],"properties":{"categories":{"type":"array","items":{"$ref":"#/components/schemas/CategoryTreeNode"}}}},"AttributeDefinition":{"type":"object","required":["key","label","type","required","filterable"],"properties":{"key":{"type":"string","example":"length"},"label":{"type":"string","example":"Length"},"type":{"type":"string","enum":["string","number","boolean","enum"],"example":"number"},"required":{"type":"boolean","example":false},"filterable":{"type":"boolean","example":true},"filterType":{"type":"string","enum":["range","select","boolean"],"example":"range"},"options":{"type":"array","items":{"type":"string"},"description":"Allowed values (only when type = enum)"},"unit":{"type":"string","example":"m"},"description":{"type":"string","example":"Overall length of the boat in meters"},"example":{"type":"string","example":"12.5"}}},"AssetTypeDefinition":{"type":"object","required":["id","label","description","categoryIds","attributes"],"properties":{"id":{"type":"string","example":"boats"},"label":{"type":"string","example":"Boats"},"description":{"type":"string","example":"Boats and watercraft — sailboats, motorboats, yachts, etc."},"categoryIds":{"type":"array","items":{"type":"string"},"example":["boats"]},"attributes":{"type":"array","items":{"$ref":"#/components/schemas/AttributeDefinition"}}}},"AssetTypeListResponse":{"type":"object","required":["results"],"properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/AssetTypeDefinition"}}}},"RegisterInput":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email","example":"user@example.com"},"password":{"type":"string","minLength":8,"maxLength":128,"example":"securepassword123"},"name":{"type":"string","minLength":1,"maxLength":100,"example":"Jane Doe"}}},"LoginInput":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email","example":"user@example.com"},"password":{"type":"string","example":"securepassword123"}}},"User":{"type":"object","required":["id","email","name","type","verified","created_at"],"properties":{"id":{"type":"string","example":"usr_01JX5KQM3HZJTN9P0X3VY6BQR"},"email":{"type":"string","format":"email","example":"user@example.com"},"name":{"type":"string","example":"Jane Doe"},"type":{"type":"string","enum":["agent","human"],"example":"human"},"verified":{"type":"boolean","example":false},"email_notifications":{"type":"boolean","example":true},"created_at":{"type":"string","format":"date-time","example":"2026-07-10T12:00:00.000Z"}}},"AuthResponse":{"type":"object","required":["user","token","token_type","expires_in"],"properties":{"user":{"$ref":"#/components/schemas/User"},"token":{"type":"string","description":"JWT session token (use as Bearer token)","example":"eyJhbGciOiJIUzI1NiJ9..."},"token_type":{"type":"string","example":"bearer"},"expires_in":{"type":"integer","description":"Token lifetime in seconds","example":604800}}},"CreateApiKeyInput":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":100,"example":"My Integration Key"},"permissions":{"type":"string","enum":["read","write","admin"],"default":"write","example":"write"}}},"ApiKeyResponse":{"type":"object","required":["id","key","key_prefix","label","permissions","created_at","message"],"properties":{"id":{"type":"string","example":"apk_01JX5KQM3HZJTN9P0X3VY6BQR"},"key":{"type":"string","description":"Plaintext API key — shown ONCE. Store securely. Format: own_<43 chars>","example":"own_aBcD123eFgH456iJkL789mNoP012qRsT345uVwX678yZ"},"key_prefix":{"type":"string","example":"own_aBcD1"},"label":{"type":"string","example":"My Integration Key"},"permissions":{"type":"string","enum":["read","write","admin"],"example":"write"},"created_at":{"type":"string","format":"date-time","example":"2026-07-10T12:00:00.000Z"},"message":{"type":"string","example":"Store this API key securely. It will not be shown again."}}},"ApiKeyListResponse":{"type":"object","required":["keys"],"properties":{"keys":{"type":"array","items":{"type":"object","required":["id","key_prefix","label","permissions","created_at"],"properties":{"id":{"type":"string"},"key_prefix":{"type":"string"},"label":{"type":"string"},"permissions":{"type":"string","enum":["read","write","admin"]},"created_at":{"type":"string","format":"date-time"},"last_used_at":{"type":"string","format":"date-time","nullable":true},"revoked_at":{"type":"string","format":"date-time","nullable":true}}}}}},"ApiKeyRevokeResponse":{"type":"object","required":["id","revoked","revoked_at"],"properties":{"id":{"type":"string"},"revoked":{"type":"boolean","example":true},"revoked_at":{"type":"string","format":"date-time"}}},"ContactInput":{"type":"object","required":["name","email","message"],"properties":{"name":{"type":"string","minLength":1,"maxLength":100,"example":"Jane Doe"},"email":{"type":"string","format":"email","maxLength":200,"example":"jane@example.com"},"message":{"type":"string","minLength":1,"maxLength":2000,"example":"Is this still available?"}}},"ContactResponse":{"type":"object","required":["message"],"properties":{"message":{"type":"string","example":"Your message has been sent to the listing owner."}}},"FlagInput":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","enum":["spam","fraud","inappropriate","duplicate","illegal","other"],"example":"spam"},"description":{"type":"string","maxLength":2000,"description":"Optional additional details","example":"This listing appears to be a duplicate."}}},"FlagResponse":{"type":"object","required":["flag_id","listing_id","listing_status","reason","created_at"],"properties":{"flag_id":{"type":"string"},"listing_id":{"type":"string"},"listing_status":{"type":"string","example":"flagged"},"reason":{"type":"string","example":"spam"},"created_at":{"type":"string","format":"date-time"}}},"AiGenerateInput":{"type":"object","required":["images"],"properties":{"images":{"type":"array","items":{"type":"string","format":"uri"},"minItems":1,"maxItems":10,"description":"Array of image URLs (already uploaded to the static service)","example":["https://static.owning.pro/temp/abc123/1.webp"]}}},"AiGenerateResponse":{"type":"object","required":["title","description","category","estimatedPriceRange","tags","condition","attributes"],"properties":{"title":{"type":"string","minLength":1,"maxLength":120,"example":"Apple iPhone 13 128GB Blue"},"description":{"type":"string","minLength":1,"maxLength":5000,"example":"Apple iPhone 13 in blue, 128GB storage. Appears to be in good condition with minor wear."},"category":{"type":"string","example":"electronics"},"estimatedPriceRange":{"type":"object","required":["min","max","currency"],"properties":{"min":{"type":"number","minimum":0,"example":350},"max":{"type":"number","minimum":0,"example":450},"currency":{"type":"string","example":"EUR"}}},"tags":{"type":"array","items":{"type":"string"},"maxItems":10,"example":["iphone","apple","smartphone","128gb"]},"condition":{"type":"string","enum":["new","like_new","good","fair","poor","refurbished"],"example":"good"},"attributes":{"type":"object","additionalProperties":true,"example":{"brand":"Apple","model":"iPhone 13","storage":"128GB"}}}},"ChangeStatusInput":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["active","paused","sold"],"example":"paused"}}},"UpdateListingInput":{"type":"object","description":"Partial update — only provided fields are changed.","properties":{"title":{"type":"string","minLength":5,"maxLength":120},"description":{"type":"string","minLength":20,"maxLength":5000},"category_id":{"type":"string"},"condition":{"type":"string","enum":["new","like_new","good","fair","poor","refurbished"]},"type":{"type":"string","enum":["sale","wanted"]},"price":{"$ref":"#/components/schemas/Price"},"location":{"$ref":"#/components/schemas/Location"},"images":{"type":"array","maxItems":10,"items":{"$ref":"#/components/schemas/Image"}},"attributes":{"type":"object","additionalProperties":true},"tags":{"type":"array","maxItems":10,"items":{"type":"string","maxLength":50}}}},"UpdateProfileInput":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"example":"Jane Doe"},"email_notifications":{"type":"boolean","example":true,"description":"Enable/disable non-essential email notifications"}}},"UserProfileResponse":{"type":"object","required":["user"],"properties":{"user":{"$ref":"#/components/schemas/User"}}},"UploadResponse":{"type":"object","required":["url","alt"],"properties":{"url":{"type":"string","format":"uri","example":"https://static.owning.pro/images/tmp-usr01-abc/1.webp"},"alt":{"type":"string","example":"photo"}}},"ListingImagesUploadResponse":{"type":"object","required":["listing_id","slug","images","uploaded"],"properties":{"listing_id":{"type":"string"},"slug":{"type":"string"},"images":{"type":"array","items":{"$ref":"#/components/schemas/Image"}},"uploaded":{"type":"integer","example":3}}},"CategoryDetail":{"type":"object","required":["id","name","slug","path","parentId","count","children"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"path":{"type":"array","items":{"type":"string"}},"parentId":{"type":"string","nullable":true},"count":{"type":"integer"},"children":{"type":"array","items":{"type":"object","required":["id","name","slug","count"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"count":{"type":"integer"}}}}}},"CategoryFlatResponse":{"type":"object","required":["categories","total"],"properties":{"categories":{"type":"array","items":{"type":"object","required":["id","name","slug","parentId","path","count"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"parentId":{"type":"string","nullable":true},"path":{"type":"array","items":{"type":"string"}},"count":{"type":"integer"}}}},"total":{"type":"integer"}}},"DeleteListingResponse":{"type":"object","required":["deleted","id"],"properties":{"deleted":{"type":"boolean","example":true},"id":{"type":"string"}}},"SchemaIndexResponse":{"type":"object","required":["schemas"],"properties":{"schemas":{"type":"array","items":{"type":"object","required":["name","url"],"properties":{"name":{"type":"string","example":"listing"},"url":{"type":"string","example":"/api/schema/listing"},"description":{"type":"string"}}}}}},"CreateListingInput":{"type":"object","required":["title","description","category_id","condition","price","location"],"properties":{"title":{"type":"string","minLength":5,"maxLength":120,"example":"Bavaria C42 Cruiser (2021)"},"description":{"type":"string","minLength":20,"maxLength":5000,"example":"Excellent family cruiser, well maintained, ready to sail."},"category_id":{"type":"string","example":"boats"},"condition":{"type":"string","enum":["new","like_new","good","fair","poor","refurbished"],"example":"good"},"type":{"type":"string","enum":["sale","wanted"],"default":"sale","example":"sale"},"price":{"$ref":"#/components/schemas/Price"},"location":{"$ref":"#/components/schemas/Location"},"images":{"type":"array","maxItems":10,"items":{"$ref":"#/components/schemas/Image"},"default":[]},"attributes":{"type":"object","additionalProperties":true,"description":"Structured attributes per the category's asset type template"},"tags":{"type":"array","maxItems":10,"items":{"type":"string","maxLength":50},"default":[],"example":["sailboat","cruiser"]},"meta":{"type":"object","properties":{"source":{"type":"string","enum":["api","web","seed","scraped"]},"language":{"type":"string"},"origin":{"$ref":"#/components/schemas/ListingOrigin"}}}}},"PublishBulkInput":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"maxItems":10000,"description":"Specific listing IDs to publish. If omitted, publishes all draft listings (or filtered by query params).","example":["lst_01JX5KQM3HZJTN9P0X3VY6BQR"]}}},"PublishBulkResult":{"type":"object","required":["published","failed","skipped"],"properties":{"published":{"type":"integer","example":50},"failed":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"reason":{"type":"string"}}}},"skipped":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"reason":{"type":"string"}}}}}},"ReimportInput":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"maxItems":1000,"description":"Specific listing IDs to reimport. If omitted, reimports all scraped listings."}}},"ReimportResult":{"type":"object","required":["reimported","skipped","errors"],"properties":{"reimported":{"type":"integer","example":712},"skipped":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"reason":{"type":"string"}}}},"errors":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"error":{"type":"string"}}}}}},"AiDiscoveryDocument":{"type":"object","required":["name","description","url","api"],"properties":{"name":{"type":"string","example":"Owning"},"description":{"type":"string","example":"Classifieds portal — buy and sell new and second-hand items"},"url":{"type":"string","format":"uri","example":"https://owning.pro"},"api":{"type":"object","required":["base_url","version","endpoints","auth","rate_limits","formats","schema"],"properties":{"base_url":{"type":"string","example":"https://owning.pro/api"},"version":{"type":"string","example":"v1"},"endpoints":{"type":"object","additionalProperties":true},"auth":{"type":"object","additionalProperties":true},"rate_limits":{"type":"object","additionalProperties":true},"formats":{"type":"array","items":{"type":"string"},"example":["json","markdown"]},"schema":{"type":"object","additionalProperties":true}}}}},"AddFavoriteInput":{"type":"object","required":["listing_id"],"properties":{"listing_id":{"type":"string","example":"lst_01JX5KQM3HZJTN9P0X3VY6BQR"}}},"FavoriteResponse":{"type":"object","required":["listing_id","favorited"],"properties":{"listing_id":{"type":"string"},"favorited":{"type":"boolean","example":true}}},"SaveSearchInput":{"type":"object","required":["name","query_params"],"properties":{"name":{"type":"string","minLength":1,"maxLength":100,"example":"Sailboats under 100k"},"query_params":{"type":"object","additionalProperties":true,"description":"Search filter parameters (query, category, price_min, price_max, condition, etc.)","example":{"q":"sailboat","price_max":"100000","condition":"good"}}}},"SavedSearchResponse":{"type":"object","required":["id","name","query_params","created_at"],"properties":{"id":{"type":"string","example":"svs_01JX5KQM3HZJTN9P0X3VY6BQR"},"name":{"type":"string","example":"Sailboats under 100k"},"query_params":{"type":"object","additionalProperties":true},"created_at":{"type":"string","format":"date-time"}}},"SavedSearchListResponse":{"type":"object","required":["results"],"properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/SavedSearchResponse"}}}},"DeleteSavedSearchResponse":{"type":"object","required":["id","deleted"],"properties":{"id":{"type":"string"},"deleted":{"type":"boolean","example":true}}},"CreateConversationInput":{"type":"object","required":["listing_id","message"],"properties":{"listing_id":{"type":"string","example":"lst_01JX5KQM3HZJTN9P0X3VY6BQR"},"message":{"type":"string","minLength":1,"maxLength":5000,"example":"Hi, is this still available?"}}},"CreateConversationResponse":{"type":"object","required":["id","message","created"],"properties":{"id":{"type":"string","example":"cnv_01JX5KQM3HZJTN9P0X3VY6BQR"},"message":{"type":"string","example":"Conversation created"},"created":{"type":"boolean","example":true}}},"SendMessageInput":{"type":"object","required":["content"],"properties":{"content":{"type":"string","minLength":1,"maxLength":5000,"example":"Can you ship to Madrid?"}}},"MessageResponse":{"type":"object","required":["id","conversation_id","sender_id","content","read_at","created_at"],"properties":{"id":{"type":"string","example":"msg_01JX5KQM3HZJTN9P0X3VY6BQR"},"conversation_id":{"type":"string"},"sender_id":{"type":"string"},"content":{"type":"string"},"read_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"ConversationListItem":{"type":"object","required":["id","listing","buyer","seller","unread_count","created_at","last_message_at"],"properties":{"id":{"type":"string"},"listing":{"type":"object","required":["id","title","slug"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"slug":{"type":"string"}}},"buyer":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"seller":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"last_message":{"type":"object","nullable":true,"properties":{"content":{"type":"string"},"sender_id":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}},"unread_count":{"type":"integer","minimum":0,"example":2},"created_at":{"type":"string","format":"date-time"},"last_message_at":{"type":"string","format":"date-time"}}},"ConversationListResponse":{"type":"object","required":["results","pagination"],"properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/ConversationListItem"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}},"ConversationDetail":{"type":"object","required":["id","listing","buyer","seller","created_at","last_message_at","messages"],"properties":{"id":{"type":"string"},"listing":{"type":"object","required":["id","title","slug"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"slug":{"type":"string"}}},"buyer":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"seller":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"created_at":{"type":"string","format":"date-time"},"last_message_at":{"type":"string","format":"date-time"},"messages":{"type":"object","required":["results","pagination"],"properties":{"results":{"type":"array","items":{"type":"object","required":["id","sender_id","sender_name","content","read_at","created_at"],"properties":{"id":{"type":"string"},"sender_id":{"type":"string"},"sender_name":{"type":"string"},"content":{"type":"string"},"read_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}},"MarkReadResponse":{"type":"object","required":["conversation_id","marked_read","read_at"],"properties":{"conversation_id":{"type":"string"},"marked_read":{"type":"integer","example":3},"read_at":{"type":"string","format":"date-time"}}},"PublicUserProfile":{"type":"object","required":["id","name","type","verified","created_at","listing_count","avg_rating","total_reviews"],"properties":{"id":{"type":"string","example":"usr_01JX5KQM3HZJTN9P0X3VY6BQR"},"name":{"type":"string","example":"Jane Doe"},"type":{"type":"string","enum":["human","agent"],"example":"human"},"verified":{"type":"boolean","example":false},"created_at":{"type":"string","format":"date-time"},"listing_count":{"type":"integer","minimum":0,"example":12},"avg_rating":{"type":"number","minimum":0,"maximum":5,"example":4.5},"total_reviews":{"type":"integer","minimum":0,"example":8}}},"ErrorResponse":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["bad_request","validation_error","moderation_rejected","unauthorized","forbidden","not_found","conflict","duplicate_listing","rate_limited","internal_error"],"example":"not_found"},"message":{"type":"string","example":"Listing not found"},"details":{"type":"object","additionalProperties":true,"description":"Additional context (e.g. Zod validation field errors)"}}}}},"PremiumCheckoutInput":{"type":"object","required":["plan","duration"],"properties":{"plan":{"type":"string","enum":["featured","urgent"],"description":"Premium plan type. 'featured' highlights the listing; 'urgent' adds an urgent badge.","example":"featured"},"duration":{"type":"integer","enum":[7,30],"description":"Duration in days for the premium promotion.","example":7}}},"PremiumCheckoutResponse":{"type":"object","required":["checkout_url","session_id","mode"],"properties":{"checkout_url":{"type":"string","format":"uri","description":"Stripe Checkout URL (or mock URL in stub mode). Redirect the user here to complete payment.","example":"https://checkout.stripe.com/c/pay/cs_test_abc123"},"session_id":{"type":"string","description":"Stripe Checkout Session ID","example":"cs_test_abc123"},"mode":{"type":"string","enum":["payment","stub"],"description":"Payment mode. 'stub' when Stripe is not configured (mock checkout).","example":"payment"}}},"PremiumStatusResponse":{"type":"object","required":["type","until","active"],"properties":{"type":{"type":"string","enum":["none","featured","urgent"],"description":"Current premium type. 'none' if not promoted.","example":"featured"},"until":{"type":"string","format":"date-time","nullable":true,"description":"Premium expiration timestamp. null if not promoted.","example":"2026-07-18T12:00:00.000Z"},"active":{"type":"boolean","description":"Whether the premium promotion is currently active.","example":true}}},"StripeWebhookResponse":{"type":"object","required":["received"],"properties":{"received":{"type":"boolean","example":true},"type":{"type":"string","description":"Stripe event type (e.g. checkout.session.completed)","example":"checkout.session.completed"}}},"StripeStatusResponse":{"type":"object","required":["mode","configured"],"properties":{"mode":{"type":"string","enum":["live","stub"],"description":"Stripe integration mode. 'stub' when STRIPE_SECRET_KEY is not set.","example":"live"},"configured":{"type":"boolean","description":"Whether Stripe is fully configured with API keys.","example":true}}},"TrackEventInput":{"type":"object","required":["event"],"properties":{"event":{"type":"string","enum":["page_view","signup","listing_created","search","listing_view"],"description":"Event type. Must be one of the whitelisted values.","example":"page_view"},"path":{"type":"string","maxLength":2048,"description":"Page path (for page_view events)","example":"/listings/lagoon-400-s2"},"referrer":{"type":"string","maxLength":2048,"description":"Referrer URL (for page_view events)"},"user_agent":{"type":"string","maxLength":512,"description":"User agent string (auto-filled from header if not provided)"},"user_id":{"type":"string","maxLength":30,"description":"User ID (for signup, listing_created events)"},"listing_id":{"type":"string","maxLength":30,"description":"Listing ID (for listing_created, listing_view events)"},"query":{"type":"string","maxLength":500,"description":"Search query (for search events)","example":"bavaria"},"results_count":{"type":"integer","minimum":0,"description":"Number of search results (for search events)"},"metadata":{"type":"object","additionalProperties":true,"description":"Any extra structured data"}}},"TrackEventResponse":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","example":true}}},"AnalyticsAdminResponse":{"type":"object","required":["range","pageViews","signups","listingsCreated","searches","listingViews","generatedAt"],"properties":{"range":{"type":"object","required":["days","from","to"],"properties":{"days":{"type":"integer","example":30},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"}}},"pageViews":{"type":"object","required":["total","byDay","topPaths"],"properties":{"total":{"type":"integer","example":15234},"byDay":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"count":{"type":"integer"}}}},"topPaths":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"count":{"type":"integer"}}}}}},"signups":{"type":"object","required":["total","byDay"],"properties":{"total":{"type":"integer","example":42},"byDay":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"count":{"type":"integer"}}}}}},"listingsCreated":{"type":"object","required":["total","byDay"],"properties":{"total":{"type":"integer","example":7},"byDay":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"count":{"type":"integer"}}}}}},"searches":{"type":"object","required":["total","topQueries"],"properties":{"total":{"type":"integer","example":3200},"topQueries":{"type":"array","items":{"type":"object","properties":{"query":{"type":"string"},"count":{"type":"integer"}}}}}},"listingViews":{"type":"object","required":["total","topListings"],"properties":{"total":{"type":"integer","example":8900},"topListings":{"type":"array","items":{"type":"object","properties":{"listing_id":{"type":"string"},"count":{"type":"integer"}}}}}},"generatedAt":{"type":"string","format":"date-time"}}},"ExpiringListingsResponse":{"type":"object","required":["results","pagination"],"properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/Listing"}},"pagination":{"$ref":"#/components/schemas/Pagination"},"days":{"type":"integer","description":"Look-ahead window in days used for the query","example":7}}},"MagicLinkInput":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","example":"user@example.com"}}},"MagicLinkResponse":{"type":"object","required":["sent"],"properties":{"sent":{"type":"boolean","example":true},"message":{"type":"string","example":"If the email exists, a magic link has been sent."}}},"AdminMetricsResponse":{"type":"object","required":["listings","users","categories"],"properties":{"listings":{"type":"object","properties":{"total":{"type":"integer","example":7668},"active":{"type":"integer","example":7668},"draft":{"type":"integer","example":0},"expired":{"type":"integer","example":0},"flagged":{"type":"integer","example":0},"sold":{"type":"integer","example":0}}},"users":{"type":"object","properties":{"total":{"type":"integer","example":15},"agents":{"type":"integer","example":1},"humans":{"type":"integer","example":14}}},"categories":{"type":"object","properties":{"total":{"type":"integer","example":12},"withListings":{"type":"integer","example":1}}}}},"AdminActionResponse":{"type":"object","required":["id","status"],"properties":{"id":{"type":"string","example":"lst_01KX..."},"status":{"type":"string","example":"active"},"message":{"type":"string","example":"Listing approved and published."}}},"ExpireListingsResponse":{"type":"object","required":["expired","errors","timestamp"],"properties":{"expired":{"type":"integer","example":12},"errors":{"type":"array","items":{"type":"string"}},"timestamp":{"type":"string","format":"date-time"}}},"NotifyExpiringResponse":{"type":"object","required":["notified","errors","timestamp"],"properties":{"notified":{"type":"integer","example":5},"errors":{"type":"array","items":{"type":"string"}},"timestamp":{"type":"string","format":"date-time"}}}}},"paths":{"/api/listings":{"get":{"tags":["Listings"],"summary":"Search listings","description":"Search and filter listings with pagination, sorting, and dynamic attribute filters. **Public** — no auth required.\n\nSupports full-text search (`q`), category filtering (hierarchical), price range, condition, location, shipping, and asset-type-specific attribute filters via `attr[...]` query params.\n\nAlso available as Markdown at `GET /api/listings.md` or by sending `Accept: text/markdown`.","operationId":"searchListings","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Full-text search query (title, description, tags)"},{"name":"category","in":"query","schema":{"type":"string"},"description":"Category ID or slug (hierarchical — includes descendants)","example":"boats"},{"name":"seller_id","in":"query","schema":{"type":"string"},"description":"Filter by seller's user ID"},{"name":"min_price","in":"query","schema":{"type":"number","minimum":0},"description":"Minimum price (inclusive). When set, unpriced listings are excluded unless include_unpriced=true.","example":50000},{"name":"max_price","in":"query","schema":{"type":"number","minimum":0},"description":"Maximum price (inclusive)","example":150000},{"name":"include_unpriced","in":"query","schema":{"type":"boolean"},"description":"Include listings without a price even when price filter is active (default: false)"},{"name":"condition","in":"query","schema":{"type":"string","enum":["new","like_new","good","fair","poor","refurbished"]},"description":"Filter by condition"},{"name":"country","in":"query","schema":{"type":"string","minLength":2,"maxLength":2},"description":"ISO 3166-1 alpha-2 country code","example":"ES"},{"name":"city","in":"query","schema":{"type":"string"},"description":"City name (case-insensitive)"},{"name":"shipping","in":"query","schema":{"type":"boolean"},"description":"Filter by shipping availability"},{"name":"type","in":"query","schema":{"type":"string","enum":["sale","wanted"]},"description":"Listing type"},{"name":"status","in":"query","schema":{"type":"string","enum":["active","paused","sold","expired","flagged","all"]},"description":"Filter by status (default: active)","example":"active"},{"name":"page","in":"query","schema":{"type":"integer","minimum":1},"description":"Page number (default: 1)","example":1},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100},"description":"Results per page (default: 20, max: 100)","example":20},{"name":"sort","in":"query","schema":{"type":"string","enum":["newest","price_asc","price_desc","relevance","random","year_desc","length_desc"]},"description":"Sort order (default: relevance). year_desc = newest boat year first, length_desc = longest boat first","example":"newest"},{"name":"attr[{key}]","in":"query","schema":{"type":"string"},"description":"Dynamic attribute filter (select type). Partial ILIKE match. Available keys depend on the category's asset type. Example: attr[boat_type]=Segelyacht. See GET /api/asset-types/{type} for available filters.","example":"attr[boat_type]=motor"},{"name":"attr[min_{key}]","in":"query","schema":{"type":"number"},"description":"Range filter minimum for numeric attributes. Example: attr[min_length]=10"},{"name":"attr[max_{key}]","in":"query","schema":{"type":"number"},"description":"Range filter maximum for numeric attributes. Example: attr[max_length]=20"}],"responses":{"200":{"description":"Paginated list of listings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedListings"},"examples":{"default":{"summary":"Default search response","value":{"results":[{"id":"lst_01JX5KQM3HZJTN9P0X3VY6BQR","slug":"bavaria-c42-cruiser-2021","title":"Bavaria C42 Cruiser (2021)","description":"Excellent family cruiser, well maintained, ready to sail.","category":{"id":"boats","name":"Boats","path":["vehicles","boats"]},"condition":"good","type":"sale","price":{"amount":89000,"currency":"EUR","negotiable":true},"location":{"country":"ES","city":"Palma de Mallorca","shipping":false},"images":[{"url":"https://images.owning.pro/listings/bavaria-c42-1.jpg"}],"seller":{"id":"usr_marketplace","name":"Owning Marketplace","type":"agent","verified":true,"member_since":"2026-07-10"},"status":"active","created_at":"2026-07-10T12:00:00.000Z","updated_at":"2026-07-10T12:00:00.000Z","expires_at":"2026-08-09T12:00:00.000Z","views":42,"tags":["sailboat","cruiser","bavaria"],"meta":{"source":"scraped","language":"en"}}],"pagination":{"page":1,"limit":20,"total":712,"pages":36}}}}},"text/markdown":{"schema":{"type":"string"},"description":"Markdown catalog of listings (when using .md suffix or Accept: text/markdown)"}}},"400":{"description":"Invalid query parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["Listings"],"summary":"Create a listing","description":"Create a new listing. **Auth required** (JWT or API key with write permission).","operationId":"createListing","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateListingInput"}}}},"responses":{"201":{"description":"Listing created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Listing"}}}},"400":{"description":"Validation error or moderation rejection","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Duplicate listing (same seller, same title + description)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/search/suggest":{"get":{"tags":["Search"],"summary":"Autocomplete suggestions","description":"Returns autocomplete suggestions for a partial search query. **Public** — no auth required.\n\nSuggestions come from three sources, merged and deduplicated:\n1. **Categories** — prefix match on category names/IDs\n2. **Popular listing titles** — trigram-accelerated partial/fuzzy match on active listings, ranked by view count\n3. **Frequent search terms** — from past search analytics (last 30 days)\n\nDesigned for client-side autocomplete with debounce (200-300ms recommended). Returns max 10 suggestions. Response time target: < 100ms.","operationId":"searchSuggest","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":100},"description":"Partial search query (min 2 characters)","example":"boat"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":10},"description":"Max suggestions to return (default: 8, max: 10)","example":8}],"responses":{"200":{"description":"Suggestions for the partial query","content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string","description":"The input query","example":"boat"},"suggestions":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["category","listing","term"],"description":"Suggestion source type"},"text":{"type":"string","description":"Suggestion text to display","example":"Boats"},"category_id":{"type":"string","description":"Category ID (when type=category)","example":"boats"},"category_path":{"type":"array","items":{"type":"string"},"description":"Category path from root (when type=category)","example":["vehicles","boats"]},"listing_id":{"type":"string","description":"Listing ID (when type=listing)"},"slug":{"type":"string","description":"Listing slug (when type=listing)"},"count":{"type":"integer","description":"Search frequency (when type=term)","example":42}},"required":["type","text"]}}},"required":["query","suggestions"]}}}},"400":{"description":"Invalid query — q must be at least 2 characters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/listings/{slug}":{"get":{"tags":["Listings"],"summary":"Get listing detail (JSON)","description":"Get a single listing by ID or slug. **Public** — no auth required.\n\nThe detail response includes the `long_description` field (if available), which is not included in list responses.","operationId":"getListing","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Listing ID or slug","example":"bavaria-c42-cruiser-2021"}],"responses":{"200":{"description":"Listing detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Listing"}}}},"404":{"description":"Listing not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"put":{"tags":["Listings"],"summary":"Update listing","description":"Update a listing. **Auth required** — owner only.","operationId":"updateListing","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Listing ID or slug"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateListingInput"}}}},"responses":{"200":{"description":"Updated listing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Listing"}}}},"403":{"description":"Not the listing owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Listing not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["Listings"],"summary":"Delete listing","description":"Soft-delete a listing (status → deleted). **Auth required** — owner only.","operationId":"deleteListing","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Listing ID or slug"}],"responses":{"200":{"description":"Listing deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteListingResponse"}}}},"403":{"description":"Not the listing owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Listing not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["Listings"],"summary":"Change listing status","description":"Change the status of a listing (active, paused, sold). **Auth required** — owner only.","operationId":"changeListingStatus","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Listing ID or slug"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeStatusInput"}}}},"responses":{"200":{"description":"Updated listing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Listing"}}}},"403":{"description":"Not the listing owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Listing not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/listings/ai-generate":{"post":{"tags":["Listings"],"summary":"Generate listing draft from photos (AI)","description":"Generate a structured listing draft from uploaded photos using AI vision. **Auth required** (JWT). Rate limited: 5 per hour per user.\n\nWhen OPENROUTER_API_KEY is not configured, the endpoint runs in **stub mode** and returns a mock draft. When the key is added, it automatically starts generating real drafts.","operationId":"aiGenerateListingDraft","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiGenerateInput"}}}},"responses":{"200":{"description":"Listing draft generated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiGenerateResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (5 per hour)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"AI service temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/listings/{slug}/publish":{"post":{"tags":["Listings"],"summary":"Publish a draft listing","description":"Move a listing from 'draft' to 'active' status. **Auth required** — owner only.","operationId":"publishListing","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Listing ID or slug"}],"responses":{"200":{"description":"Listing published","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Listing"}}}},"403":{"description":"Not the listing owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Listing not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Listing is not in draft status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/listings/{slug}/mark-sold":{"post":{"tags":["Listings"],"summary":"Mark listing as sold","description":"Move a listing to 'sold' status. **Auth required** — owner only. Listing must be active or paused.","operationId":"markListingSold","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Listing ID or slug"}],"responses":{"200":{"description":"Listing marked as sold","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Listing"}}}},"403":{"description":"Not the listing owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Listing not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/listings/{slug}/renew":{"post":{"tags":["Listings"],"summary":"Renew a listing","description":"Renew a listing — extends `expires_at` by 30 days from now and sets status to 'active'. **Auth required** — owner only.\n\nListings in 'expired', 'active', or 'paused' status can be renewed. This is useful for sellers whose listings have expired or are about to expire and want to keep them visible.","operationId":"renewListing","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Listing ID or slug"}],"responses":{"200":{"description":"Listing renewed (30-day extension, status → active)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Listing"}}}},"403":{"description":"Not the listing owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Listing not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/listings/{slug}/contact":{"post":{"tags":["Listings"],"summary":"Contact listing owner","description":"Send a message to the listing owner via email. **Public** — no auth required. The seller's email is never exposed. Rate limited: 5 per hour per IP.","operationId":"contactListingOwner","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Listing ID or slug"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactInput"}}}},"responses":{"201":{"description":"Message sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactResponse"}}}},"404":{"description":"Listing not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded (5 per hour)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/listings/{slug}/flag":{"post":{"tags":["Listings"],"summary":"Flag a listing for moderation","description":"Flag a listing for moderation review. **Auth required**. The listing transitions to 'flagged' status.","operationId":"flagListing","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Listing ID or slug"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagInput"}}}},"responses":{"201":{"description":"Listing flagged","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Listing not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/listings/{slug}/similar":{"get":{"tags":["Listings"],"summary":"Get similar listings","description":"Get 4-6 listings that are similar to the given listing. **Public** — no auth required.\n\nSimilarity is based on:\n1. Same category\n2. Price within ±20% of the source listing\n3. Same brand (if the source listing has a brand in `boat_specs`)\n\nThe endpoint uses a progressive fallback strategy: it first tries brand + price match, then price match only, then category match only — ensuring the RelatedListings component always has content to show.\n\nOnly active, non-merged (dedup_status='unique') listings are returned. The source listing itself is excluded.","operationId":"getSimilarListings","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Listing ID or slug","example":"bavaria-c42-cruiser-2021"}],"responses":{"200":{"description":"Similar listings","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","maxItems":6,"items":{"$ref":"#/components/schemas/Listing"}}}}}}},"404":{"description":"Listing not found (empty results array returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/listings/{slug}.md":{"get":{"tags":["Listings"],"summary":"Get listing detail (Markdown)","description":"Get a single listing as machine-readable Markdown. **Public** — no auth required.\n\nThe Markdown includes YAML frontmatter with key metadata, followed by structured sections for specs, description, and images.","operationId":"getListingMarkdown","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Listing ID or slug","example":"bavaria-c42-cruiser-2021"}],"responses":{"200":{"description":"Listing as Markdown","content":{"text/markdown":{"schema":{"type":"string"}}}},"404":{"description":"Listing not found","content":{"text/markdown":{"schema":{"type":"string"}}}}}}},"/api/listings.md":{"get":{"tags":["Listings"],"summary":"Get listings catalog (Markdown)","description":"Get the full listings catalog as Markdown. **Public** — no auth required.\n\nReturns a Markdown document with all listings (respecting the same query parameters as GET /api/listings). Useful for agents that want to browse the catalog in a single request.","operationId":"getListingsMarkdown","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Full-text search query"},{"name":"category","in":"query","schema":{"type":"string"},"description":"Category ID or slug","example":"boats"},{"name":"page","in":"query","schema":{"type":"integer","minimum":1},"description":"Page number (default: 1)"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100},"description":"Results per page (default: 20, max: 100)"}],"responses":{"200":{"description":"Listings catalog as Markdown","content":{"text/markdown":{"schema":{"type":"string"}}}}}}},"/api/asset-types":{"get":{"tags":["Asset Types"],"summary":"List all asset types","description":"List all available asset type templates. **Public** — no auth required.\n\nEach asset type defines the structured attributes available for listings in its associated categories.","operationId":"listAssetTypes","responses":{"200":{"description":"List of asset type definitions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetTypeListResponse"}}}}}}},"/api/asset-types/{type}":{"get":{"tags":["Asset Types"],"summary":"Get asset type template (JSON)","description":"Get a specific asset type definition with all its attributes. **Public** — no auth required.\n\nThe response includes which attributes are filterable and their filter types (range, select, boolean), which is needed to construct `attr[...]` query parameters for GET /api/listings.","operationId":"getAssetType","parameters":[{"name":"type","in":"path","required":true,"schema":{"type":"string"},"description":"Asset type ID","example":"boats"}],"responses":{"200":{"description":"Asset type definition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetTypeDefinition"}}}},"404":{"description":"Asset type not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/asset-types/{type}.md":{"get":{"tags":["Asset Types"],"summary":"Get asset type template (Markdown)","description":"Get a specific asset type definition as Markdown. **Public** — no auth required.\n\nIncludes YAML frontmatter, an attributes table, and a filters section with examples.","operationId":"getAssetTypeMarkdown","parameters":[{"name":"type","in":"path","required":true,"schema":{"type":"string"},"description":"Asset type ID","example":"boats"}],"responses":{"200":{"description":"Asset type as Markdown","content":{"text/markdown":{"schema":{"type":"string"}}}},"404":{"description":"Asset type not found","content":{"text/markdown":{"schema":{"type":"string"}}}}}}},"/api/categories":{"get":{"tags":["Categories"],"summary":"Get category tree","description":"Get the full category tree with listing counts. **Public** — no auth required.\n\nThe tree is hierarchical with `count` (active listings) and `children` on each node. Use `?hide_empty=true` to prune categories with zero listings.","operationId":"getCategoryTree","parameters":[{"name":"hide_empty","in":"query","schema":{"type":"boolean"},"description":"Hide categories with zero listings (parents with non-empty children are kept)"}],"responses":{"200":{"description":"Category tree with listing counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CategoryTreeResponse"}}}}}}},"/api/categories/flat":{"get":{"tags":["Categories"],"summary":"Get flat category list","description":"Get all categories as a flat list with counts. **Public** — no auth required.","operationId":"getCategoryFlat","parameters":[{"name":"hide_empty","in":"query","schema":{"type":"boolean"},"description":"Hide categories with zero listings"}],"responses":{"200":{"description":"Flat category list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CategoryFlatResponse"}}}}}}},"/api/categories/{id}":{"get":{"tags":["Categories"],"summary":"Get category detail","description":"Get a single category by ID or slug, with its direct children and counts. **Public**.","operationId":"getCategoryDetail","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Category ID or slug","example":"boats"}],"responses":{"200":{"description":"Category detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CategoryDetail"}}}},"404":{"description":"Category not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/schema/listing":{"get":{"tags":["Schema"],"summary":"Get listing JSON Schema","description":"Get the JSON Schema (draft 2020-12) for the Listing data type. **Public** — no auth required.\n\nUse this to validate listing data before submitting via POST /api/listings, or to understand the exact shape of listing objects.","operationId":"getListingSchema","responses":{"200":{"description":"JSON Schema for the Listing type","content":{"application/schema+json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/schema":{"get":{"tags":["Schema"],"summary":"Get schema index","description":"List all available JSON schemas. **Public**.","operationId":"getSchemaIndex","responses":{"200":{"description":"Schema index","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SchemaIndexResponse"}}}}}}},"/api/schema/listing.md":{"get":{"tags":["Schema"],"summary":"Get listing schema as Markdown","description":"Get the listing schema in Markdown format for AI agents. **Public**.","operationId":"getListingSchemaMarkdown","responses":{"200":{"description":"Listing schema as Markdown","content":{"text/markdown":{"schema":{"type":"string"}}}}}}},"/api/auth/register":{"post":{"tags":["Auth"],"summary":"Register a new account","description":"Register with email and password. **Public** — no auth required.\n\nReturns a JWT session token immediately upon successful registration.","operationId":"register","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterInput"}}}},"responses":{"201":{"description":"Account created, session token returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}},"409":{"description":"Email already registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/auth/login":{"post":{"tags":["Auth"],"summary":"Login","description":"Login with email and password. **Public** — no auth required.\n\nReturns a JWT session token valid for 7 days.","operationId":"login","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginInput"}}}},"responses":{"200":{"description":"Login successful, session token returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}},"401":{"description":"Invalid email or password","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/auth/magic-link":{"post":{"tags":["Auth"],"summary":"Request a magic link (passwordless login)","description":"Request a magic link to be sent to the given email. **Public** — no auth required.\n\nIf the email exists, a one-time login link is sent. In stub mode (no email provider configured), the link is logged to the server console instead of being emailed.","operationId":"requestMagicLink","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MagicLinkInput"}}}},"responses":{"200":{"description":"Magic link sent (or logged in stub mode)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MagicLinkResponse"}}}}}}},"/api/api-keys":{"post":{"tags":["Auth"],"summary":"Create an API key","description":"Generate a new API key. **Auth required** (JWT session).\n\nThe plaintext key is returned **once** — store it securely. It cannot be retrieved later. Use the key in the `Authorization: Bearer own_...` header for programmatic API access.","operationId":"createApiKey","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKeyInput"}}}},"responses":{"201":{"description":"API key created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["Auth"],"summary":"List API keys","description":"List the authenticated user's API keys (without plaintext). **Auth required**.","operationId":"listApiKeys","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List of API keys","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyListResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/api-keys/{id}":{"delete":{"tags":["Auth"],"summary":"Revoke an API key","description":"Revoke (soft-delete) an API key. **Auth required** — only the key owner can revoke.","operationId":"revokeApiKey","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"API key ID"}],"responses":{"200":{"description":"API key revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyRevokeResponse"}}}},"403":{"description":"Not the key owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"API key not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/me":{"get":{"tags":["Auth"],"summary":"Get current user profile","description":"Get the authenticated user's profile. **Auth required**.","operationId":"getMe","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"User profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserProfileResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"put":{"tags":["Auth"],"summary":"Update user profile","description":"Update the authenticated user's profile (name). **Auth required**.","operationId":"updateMe","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileInput"}}}},"responses":{"200":{"description":"Updated profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserProfileResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/me/listings":{"get":{"tags":["Listings","Auth"],"summary":"Get my listings","description":"Get the authenticated user's own listings (all statuses, including drafts). **Auth required**.","operationId":"getMyListings","security":[{"bearerAuth":[]}],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["all","draft","active","paused","sold","expired","flagged","deleted"]},"description":"Filter by status (default: all)"},{"name":"page","in":"query","schema":{"type":"integer","minimum":1},"description":"Page number (default: 1)"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100},"description":"Results per page (default: 20)"},{"name":"sort","in":"query","schema":{"type":"string","enum":["newest","oldest"]},"description":"Sort order (default: newest)"}],"responses":{"200":{"description":"Paginated list of user's listings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedListings"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/me/listings/expiring":{"get":{"tags":["Listings","Auth"],"summary":"Get my expiring listings","description":"Get the authenticated user's listings that are expiring within N days (default 7). **Auth required**.\n\nUseful for sellers to see which listings need renewal before they expire. Listings expire 30 days after creation.","operationId":"getExpiringListings","security":[{"bearerAuth":[]}],"parameters":[{"name":"days","in":"query","schema":{"type":"integer","minimum":1,"maximum":30},"description":"Look-ahead window in days (default: 7, max: 30)","example":7},{"name":"page","in":"query","schema":{"type":"integer","minimum":1},"description":"Page number (default: 1)"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100},"description":"Items per page (default: 50, max: 100)"}],"responses":{"200":{"description":"Paginated list of expiring listings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpiringListingsResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/upload":{"post":{"tags":["Upload"],"summary":"Upload a single image","description":"Upload a single image (no listing required). Used by the sell wizard. **Auth required** (write permission). Image is converted to webp. Rate limited: 50 images per user per day.","operationId":"uploadImage","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"Image file (jpg, png, webp, heic, heif, avif). Max 10MB."}}}}}},"responses":{"201":{"description":"Image uploaded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadResponse"}}}},"400":{"description":"Invalid file type or size","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Daily upload limit exceeded (50/day)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/listings/{slug}/images":{"post":{"tags":["Upload","Listings"],"summary":"Upload images for a listing","description":"Upload images for an existing listing (owner only). **Auth required** (write permission). Up to 10 images per request. Rate limited: 50 images per user per day.","operationId":"uploadListingImages","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Listing ID or slug"}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"files":{"type":"array","maxItems":10,"items":{"type":"string","format":"binary"},"description":"Image files (jpg, png, webp, heic, heif, avif). Max 10MB each."}}}}}},"responses":{"201":{"description":"Images uploaded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListingImagesUploadResponse"}}}},"400":{"description":"Invalid file type or size","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Not the listing owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Listing not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Daily upload limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/openapi.json":{"get":{"tags":["Discovery"],"summary":"OpenAPI specification","description":"Get the OpenAPI 3.1 specification as JSON. **Public**.","operationId":"getOpenApiSpec","responses":{"200":{"description":"OpenAPI 3.1 JSON spec","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/docs":{"get":{"tags":["Discovery"],"summary":"Interactive API docs (Scalar)","description":"Interactive API reference UI powered by Scalar. **Public**.","operationId":"getApiDocs","responses":{"200":{"description":"HTML page with Scalar API reference","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/health":{"get":{"tags":["Discovery"],"summary":"Health check","description":"Service health check with database connectivity status. **Public**.","operationId":"healthCheck","responses":{"200":{"description":"Service is healthy","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"Service degraded (database unavailable)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/robots.txt":{"get":{"tags":["Discovery"],"summary":"Robots.txt","description":"Standard robots.txt with AI agent discovery directive. **Public**.","operationId":"getRobotsTxt","responses":{"200":{"description":"Robots.txt","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/api/listings/{slug}/premium":{"post":{"tags":["Stripe","Listings"],"summary":"Initiate premium checkout (Stripe)","description":"Initiate a Stripe Checkout session to promote a listing. **Auth required** — owner only.\n\nThe listing must be in 'active' status. Returns a Stripe Checkout URL to redirect the user to. In stub mode (no STRIPE_SECRET_KEY), returns a mock checkout URL.","operationId":"initiatePremiumCheckout","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Listing ID or slug"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PremiumCheckoutInput"}}}},"responses":{"200":{"description":"Checkout session created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PremiumCheckoutResponse"}}}},"400":{"description":"Listing is not active","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Not the listing owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Listing not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/listings/{slug}/premium-status":{"get":{"tags":["Stripe","Listings"],"summary":"Check premium status","description":"Check the premium promotion status of a listing. **Public** — no auth required.","operationId":"getPremiumStatus","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Listing ID or slug"}],"responses":{"200":{"description":"Premium status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PremiumStatusResponse"}}}},"404":{"description":"Listing not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/webhooks/stripe":{"post":{"tags":["Stripe"],"summary":"Stripe webhook receiver","description":"Receives Stripe webhook events. **No auth** — signature verification via `Stripe-Signature` header.\n\nIn live mode, the raw body is verified using STRIPE_WEBHOOK_SECRET. In stub mode, the body is parsed directly. Handles `checkout.session.completed` events to activate premium promotions.","operationId":"stripeWebhook","responses":{"200":{"description":"Webhook received and processed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeWebhookResponse"}}}},"400":{"description":"Invalid signature","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/stripe/status":{"get":{"tags":["Stripe"],"summary":"Stripe service status","description":"Check the Stripe integration status (live vs stub mode). **Public** — no auth required.","operationId":"getStripeStatus","responses":{"200":{"description":"Stripe service status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StripeStatusResponse"}}}}}}},"/api/track":{"post":{"tags":["Analytics"],"summary":"Track an analytics event","description":"Public endpoint (no auth) that receives analytics events from the frontend. Fire-and-forget — always returns 202 Accepted, even if the DB write fails. Rate limited per IP.\n\nValid event types: `page_view`, `signup`, `listing_created`, `search`, `listing_view`. IP addresses are hashed (SHA-256) for privacy — raw IPs are never stored.","operationId":"trackEvent","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackEventInput"}}}},"responses":{"202":{"description":"Event received (best-effort processing)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackEventResponse"}}}},"400":{"description":"Invalid event type or payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/me/favorites":{"get":{"tags":["Favorites"],"summary":"List user's favorited listings","description":"Returns the authenticated user's favorited listings, paginated. **Auth required**.","operationId":"listFavorites","security":[{"bearerAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1},"description":"Page number (default: 1)"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50},"description":"Items per page (default: 20)"}],"responses":{"200":{"description":"Paginated favorited listings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedListings"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["Favorites"],"summary":"Add a listing to favorites","description":"Bookmarks a listing for the authenticated user. Idempotent — adding the same listing twice is a no-op. **Auth required**.","operationId":"addFavorite","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddFavoriteInput"}}}},"responses":{"201":{"description":"Listing added to favorites","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FavoriteResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Listing not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/me/favorites/{listingId}":{"delete":{"tags":["Favorites"],"summary":"Remove a listing from favorites","description":"Removes a listing from the authenticated user's favorites. Idempotent — returns 200 even if the listing was not favorited. **Auth required**.","operationId":"removeFavorite","security":[{"bearerAuth":[]}],"parameters":[{"name":"listingId","in":"path","required":true,"schema":{"type":"string"},"description":"Listing ID to remove from favorites"}],"responses":{"200":{"description":"Listing removed from favorites","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FavoriteResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/me/saved-searches":{"get":{"tags":["Saved Searches"],"summary":"List user's saved searches","description":"Returns all saved searches for the authenticated user. **Auth required**.","operationId":"listSavedSearches","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List of saved searches","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchListResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["Saved Searches"],"summary":"Save a search","description":"Saves a search query with its filter parameters for later re-use. **Auth required**.","operationId":"saveSearch","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveSearchInput"}}}},"responses":{"201":{"description":"Search saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/me/saved-searches/{id}":{"delete":{"tags":["Saved Searches"],"summary":"Delete a saved search","description":"Deletes a saved search. Only the owner can delete their saved searches. **Auth required**.","operationId":"deleteSavedSearch","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Saved search ID"}],"responses":{"200":{"description":"Saved search deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteSavedSearchResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Not the owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Saved search not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/conversations":{"get":{"tags":["Messaging"],"summary":"List user's conversations","description":"Returns conversations where the authenticated user is the buyer or seller, sorted by last message time. Includes unread count and last message preview. **Auth required**.","operationId":"listConversations","security":[{"bearerAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1},"description":"Page number (default: 1)"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50},"description":"Items per page (default: 20)"}],"responses":{"200":{"description":"Paginated conversations","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationListResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["Messaging"],"summary":"Create a conversation with first message","description":"Starts a new conversation about a listing with an initial message. If a conversation already exists for this listing + buyer, the message is appended to it. Cannot message your own listing. **Auth required**.","operationId":"createConversation","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateConversationInput"}}}},"responses":{"200":{"description":"Message added to existing conversation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateConversationResponse"}}}},"201":{"description":"Conversation created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateConversationResponse"}}}},"400":{"description":"Cannot message your own listing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Listing not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/conversations/{id}":{"get":{"tags":["Messaging"],"summary":"Get conversation detail with messages","description":"Returns a conversation with paginated messages (oldest first). Only participants (buyer or seller) can view. **Auth required**.","operationId":"getConversation","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Conversation ID"},{"name":"page","in":"query","schema":{"type":"integer","minimum":1},"description":"Message page number (default: 1)"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100},"description":"Messages per page (default: 50)"}],"responses":{"200":{"description":"Conversation detail with messages","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationDetail"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Not a participant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Conversation not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/conversations/{id}/messages":{"post":{"tags":["Messaging"],"summary":"Send a message in a conversation","description":"Adds a message to an existing conversation. Only participants (buyer or seller) can send messages. **Auth required**.","operationId":"sendMessage","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Conversation ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageInput"}}}},"responses":{"201":{"description":"Message sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Not a participant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Conversation not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/conversations/{id}/read":{"patch":{"tags":["Messaging"],"summary":"Mark conversation messages as read","description":"Marks all unread messages not sent by the current user as read. Only participants can mark messages. **Auth required**.","operationId":"markConversationRead","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Conversation ID"}],"responses":{"200":{"description":"Messages marked as read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkReadResponse"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Not a participant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Conversation not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/users/{id}":{"get":{"tags":["User Profiles"],"summary":"Get public user profile","description":"Returns a user's public profile: name, type, verified status, member since date, active listing count, and aggregate rating. **No email or sensitive data.** Public — no auth required.","operationId":"getUserProfile","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"User ID"}],"responses":{"200":{"description":"Public user profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicUserProfile"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/users/{id}/listings":{"get":{"tags":["User Profiles","Listings"],"summary":"List user's active listings","description":"Returns a user's active listings, paginated. Public — no auth required.","operationId":"getUserListings","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"User ID"},{"name":"page","in":"query","schema":{"type":"integer","minimum":1},"description":"Page number (default: 1)"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50},"description":"Items per page (default: 20)"}],"responses":{"200":{"description":"Paginated active listings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedListings"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/.well-known/ai.json":{"get":{"tags":["Discovery"],"summary":"Agent discovery document","description":"Machine-readable description of the API surface for agent auto-discovery. **Public** — no auth required.\n\nReturns endpoint paths, auth scheme, rate limits, response formats, and schema references. Also discoverable via the `AI-Discovery` directive in `/robots.txt`.","operationId":"getAiDiscovery","responses":{"200":{"description":"Agent discovery document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiDiscoveryDocument"}}}}}}}}}