{"openapi":"3.1.0","info":{"title":"Vakantio API","version":"1.0.0","description":"Public API for vakantio.de — personal travel blogs with posts, photos, GPX routes, trips (itineraries), and destinations. Reading public content is unauthenticated; authoring requires a Bearer token. Agent-friendly alternatives: the hosted MCP server at https://mcp.vakantio.de/mcp and the `@vakantio/cli` npm package. Machine-readable guides: https://vakantio.de/llms.txt\n\nVersioning & deprecation policy: this API is a single stable, unversioned surface — the same one the official SDK, CLI, WhatsApp agent, and MCP server run on, so documented endpoints change additively only. If an endpoint ever needs a breaking change, the old behavior keeps working and responses gain Deprecation/Sunset headers at least 90 days before removal, announced in the changelog of @vakantio/sdk (which is updated in lockstep).","contact":{"name":"Vakantio","email":"info@vakantio.de","url":"https://vakantio.de"}},"servers":[{"url":"https://api.vakantio.de"}],"security":[{"bearerSession":[]},{"oauth2":["vakantio:full"]},{}],"components":{"securitySchemes":{"oauth2":{"type":"oauth2","description":"OAuth 2.1 authorization code + PKCE with dynamic client registration (https://api.vakantio.de/register). Used automatically by MCP clients.","flows":{"authorizationCode":{"authorizationUrl":"https://api.vakantio.de/authorize","tokenUrl":"https://api.vakantio.de/token","scopes":{"vakantio:full":"Full access to the authorizing user's account: read and write posts, drafts, blogs, trips, and comments."}}}},"bearerSession":{"type":"http","scheme":"bearer","description":"Vakantio session token, e.g. from `vakantio login` (loopback) or `vakantio login --device` (RFC 8628). Equivalent to an OAuth access token."}},"schemas":{"PostSummary":{"type":"object","properties":{"_id":{"type":"string"},"title":{"type":"string"},"preview":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"topics":{"type":"array","items":{"type":"string"}},"path":{"type":"string","description":"URL slug (assigned at first publish)"},"fullPath":{"type":"string","description":"Public URL path: /<blogPath>/<slug>"},"blogName":{"type":"string"},"likeCount":{"type":"number"},"commentCount":{"type":"number"},"datePublished":{"type":"number","description":"Unix ms"},"dateTraveled":{"type":"number","description":"Unix ms"}}},"Error":{"type":"object","description":"Structured error body returned for every non-2xx response.","properties":{"error":{"type":"string","description":"Stable machine-readable code: not_found, gone, not_authenticated, request_failed, server_error."},"message":{"type":"string","description":"Human-readable detail (optional)"},"hint":{"type":"string","description":"Where to look next (optional)"}},"required":["error"]}}},"paths":{"/search":{"get":{"operationId":"search","summary":"Full-text search across posts, blogs, and destinations","description":"Free-text search across published posts, blogs, and destinations. No authentication required. Returns grouped results; post items include their public URL path (fullPath).","security":[{}],"parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"},"description":"Free-text query"}],"responses":{"200":{"description":"Grouped search results","content":{"application/json":{"schema":{"type":"object","properties":{"posts":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/PostSummary"}}}},"blogs":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object"}}}},"destinations":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object"}}}}}}}}},"4XX":{"description":"Client error — JSON body with a stable `error` code (e.g. not_found, not_authenticated, request_failed) and an optional `hint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Server error — JSON body with `error: \"server_error\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/posts":{"get":{"operationId":"listPosts","summary":"List public posts with filters and pagination","description":"List published, publicly visible posts, newest first. Filter by tag, topic slugs, destination, or locale; paginate with page (30/page) or skip/limit. No authentication required.","security":[{}],"parameters":[{"name":"tags","in":"query","schema":{"type":"string"},"description":"Filter by tag"},{"name":"topics","in":"query","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true,"description":"Filter by topic slugs (repeat the parameter)"},{"name":"destinationId","in":"query","schema":{"type":"string"}},{"name":"locale","in":"query","schema":{"type":"string","enum":["de","en"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":30}},{"name":"skip","in":"query","schema":{"type":"integer"}},{"name":"page","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"Post list","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/PostSummary"}},"totalCount":{"type":"integer"}}}}}},"4XX":{"description":"Client error — JSON body with a stable `error` code (e.g. not_found, not_authenticated, request_failed) and an optional `hint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Server error — JSON body with `error: \"server_error\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/posts/geo":{"get":{"operationId":"listPostsGeo","summary":"List public posts inside a geographic bounding box","description":"Posts with coordinates inside a bounding box, for map views and location-based research. Convert a center+radius search to a box before calling. No authentication required.","security":[{}],"parameters":[{"name":"nelat","in":"query","required":true,"schema":{"type":"number"},"description":"North latitude"},{"name":"swlat","in":"query","required":true,"schema":{"type":"number"},"description":"South latitude"},{"name":"nelng","in":"query","required":true,"schema":{"type":"number"},"description":"East longitude"},{"name":"swlng","in":"query","required":true,"schema":{"type":"number"},"description":"West longitude"},{"name":"limit","in":"query","schema":{"type":"integer","default":10}}],"responses":{"200":{"description":"Posts with coordinates","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PostSummary"}}}}},"4XX":{"description":"Client error — JSON body with a stable `error` code (e.g. not_found, not_authenticated, request_failed) and an optional `hint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Server error — JSON body with `error: \"server_error\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/blog/post":{"get":{"operationId":"getPost","summary":"Get a single post by id or by blogPath + postPath","description":"Public for published posts; drafts require auth (`draft=true`).","security":[{},{"bearerSession":[]},{"oauth2":["vakantio:full"]}],"parameters":[{"name":"_id","in":"query","schema":{"type":"string"},"description":"Post id (alternative to paths)"},{"name":"blogPath","in":"query","schema":{"type":"string"}},{"name":"postPath","in":"query","schema":{"type":"string"},"description":"Post slug"},{"name":"draft","in":"query","schema":{"type":"boolean"},"description":"Return the draft working copy (auth)"},{"name":"locale","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"The post (full content, media, comments)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostSummary"}}}},"4XX":{"description":"Client error — JSON body with a stable `error` code (e.g. not_found, not_authenticated, request_failed) and an optional `hint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Server error — JSON body with `error: \"server_error\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createOrUpdatePost","summary":"Create or update a post/draft","description":"Create (no `_id`) or update (with `_id`) a post. Set `draft: true` to work on the draft copy. Requires authentication: OAuth 2.1 access token or a vakantio session token (from `vakantio login`), sent as `Authorization: Bearer <token>`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"_id":{"type":"string","description":"Omit to create"},"title":{"type":"string"},"content":{"type":"string","description":"HTML body"},"blogPath":{"type":"string"},"draft":{"type":"boolean"},"tags":{"type":"array","items":{"type":"string"}},"topics":{"type":"array","items":{"type":"string"}},"locale":{"type":"string","enum":["de","en"]},"dateTraveled":{"type":"number","description":"Unix ms"},"images":{"type":"array","items":{"type":"object"},"description":"Attached media objects"}},"required":["blogPath"]}}}},"responses":{"200":{"description":"The saved post","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostSummary"}}}},"4XX":{"description":"Client error — JSON body with a stable `error` code (e.g. not_found, not_authenticated, request_failed) and an optional `hint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Server error — JSON body with `error: \"server_error\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/blog/post/state":{"post":{"operationId":"changePostState","summary":"Publish, unpublish (delete), or delete a draft","description":"Pass exactly one of `publish`, `delete`, `deleteDraft` as true together with `postId`. Requires authentication: OAuth 2.1 access token or a vakantio session token (from `vakantio login`), sent as `Authorization: Bearer <token>`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"postId":{"type":"string"},"publish":{"type":"boolean"},"delete":{"type":"boolean"},"deleteDraft":{"type":"boolean"}},"required":["postId"]}}}},"responses":{"200":{"description":"New blog counters","content":{"application/json":{"schema":{"type":"object","properties":{"_id":{"type":"string"},"blog":{"type":"object","properties":{"postCount":{"type":"integer"},"draftCount":{"type":"integer"},"deletedCount":{"type":"integer"}}}}}}}},"4XX":{"description":"Client error — JSON body with a stable `error` code (e.g. not_found, not_authenticated, request_failed) and an optional `hint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Server error — JSON body with `error: \"server_error\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/like-post":{"post":{"operationId":"likePost","summary":"Like or unlike a post (toggles)","description":"Requires authentication: OAuth 2.1 access token or a vakantio session token (from `vakantio login`), sent as `Authorization: Bearer <token>`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"postId":{"type":"string"}},"required":["postId"]}}}},"responses":{"200":{"description":"Updated post","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostSummary"}}}},"4XX":{"description":"Client error — JSON body with a stable `error` code (e.g. not_found, not_authenticated, request_failed) and an optional `hint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Server error — JSON body with `error: \"server_error\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/blog":{"get":{"operationId":"getBlog","summary":"Get a blog (settings, aggregate stats, authors) by its URL path","description":"Blog settings, aggregate statistics (views, likes, countries), and author list, looked up by the blog's URL path (the segment in https://vakantio.de/<path>). No authentication required for public blogs.","security":[{}],"parameters":[{"name":"path","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The blog","content":{"application/json":{"schema":{"type":"object"}}}},"4XX":{"description":"Client error — JSON body with a stable `error` code (e.g. not_found, not_authenticated, request_failed) and an optional `hint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Server error — JSON body with `error: \"server_error\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/blog/posts":{"get":{"operationId":"listBlogPosts","summary":"List a blog's published posts (paginated, 30/page)","description":"A single blog's published posts in reverse-chronological order, 30 per page. No authentication required for public blogs.","security":[{}],"parameters":[{"name":"path","in":"query","required":true,"schema":{"type":"string"},"description":"Blog URL path"},{"name":"page","in":"query","schema":{"type":"integer","default":1}}],"responses":{"200":{"description":"Post list","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/PostSummary"}},"totalCount":{"type":"integer"}}}}}},"4XX":{"description":"Client error — JSON body with a stable `error` code (e.g. not_found, not_authenticated, request_failed) and an optional `hint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Server error — JSON body with `error: \"server_error\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/blog/drafts":{"get":{"operationId":"listBlogDrafts","summary":"List a blog's draft posts","description":"Requires authentication: OAuth 2.1 access token or a vakantio session token (from `vakantio login`), sent as `Authorization: Bearer <token>`.","parameters":[{"name":"path","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Draft list","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/PostSummary"}},"totalCount":{"type":"integer"}}}}}},"4XX":{"description":"Client error — JSON body with a stable `error` code (e.g. not_found, not_authenticated, request_failed) and an optional `hint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Server error — JSON body with `error: \"server_error\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/blog/stats":{"get":{"operationId":"getBlogStats","summary":"Daily views (90 days), view trends, countries and cities visited","description":"Requires authentication: OAuth 2.1 access token or a vakantio session token (from `vakantio login`), sent as `Authorization: Bearer <token>`.","parameters":[{"name":"path","in":"query","required":true,"schema":{"type":"string"}},{"name":"locale","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Blog statistics","content":{"application/json":{"schema":{"type":"object"}}}},"4XX":{"description":"Client error — JSON body with a stable `error` code (e.g. not_found, not_authenticated, request_failed) and an optional `hint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Server error — JSON body with `error: \"server_error\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/destinations":{"get":{"operationId":"listDestinations","summary":"List destinations (countries, states, cities)","description":"Destinations in the vakantio geography tree. Without parentId lists top-level countries; pass a destination id as parentId to drill into its states and cities. No authentication required.","security":[{}],"parameters":[{"name":"parentId","in":"query","schema":{"type":"string"},"description":"Drill into a destination's children"},{"name":"type","in":"query","schema":{"type":"string","enum":["country","state","city"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":20}}],"responses":{"200":{"description":"Destination list","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"code":{"type":"string"}}}}}}}}},"4XX":{"description":"Client error — JSON body with a stable `error` code (e.g. not_found, not_authenticated, request_failed) and an optional `hint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Server error — JSON body with `error: \"server_error\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/destination/{destinationId}":{"get":{"operationId":"getDestination","summary":"Get a single destination by id","description":"One destination with its metadata (name, ISO code, parent chain). Destination ids appear in search results and post objects. No authentication required.","security":[{}],"parameters":[{"name":"destinationId","in":"path","required":true,"schema":{"type":"string"}},{"name":"locale","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"The destination","content":{"application/json":{"schema":{"type":"object"}}}},"4XX":{"description":"Client error — JSON body with a stable `error` code (e.g. not_found, not_authenticated, request_failed) and an optional `hint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Server error — JSON body with `error: \"server_error\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/blog/{blogPath}/trips":{"get":{"operationId":"listTrips","summary":"List a blog's trips (multi-post itineraries)","description":"A blog's trips — named multi-post itineraries with a date range. Posts are attached to trips; use the trip id in the public URL https://vakantio.de/<locale>/trip/<id>. No authentication required for public trips.","security":[{}],"parameters":[{"name":"blogPath","in":"path","required":true,"schema":{"type":"string"}},{"name":"locale","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Trip list","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}},"4XX":{"description":"Client error — JSON body with a stable `error` code (e.g. not_found, not_authenticated, request_failed) and an optional `hint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Server error — JSON body with `error: \"server_error\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/user":{"get":{"operationId":"getCurrentUser","summary":"Get the authenticated user's profile and blogs","description":"Requires authentication: OAuth 2.1 access token or a vakantio session token (from `vakantio login`), sent as `Authorization: Bearer <token>`.","responses":{"200":{"description":"The user","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Not authenticated"},"4XX":{"description":"Client error — JSON body with a stable `error` code (e.g. not_found, not_authenticated, request_failed) and an optional `hint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Server error — JSON body with `error: \"server_error\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/auth/device/code":{"post":{"operationId":"startDeviceAuthorization","summary":"Start an RFC 8628 device authorization (headless login)","description":"Begin an RFC 8628 device-authorization login for environments that cannot open a browser callback (containers, CI, sandboxes). Show the user_code and verification_uri to the user, then poll the token endpoint.","security":[{}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"client_name":{"type":"string"},"client_version":{"type":"string"}}}}}},"responses":{"200":{"description":"Device + user codes per RFC 8628","content":{"application/json":{"schema":{"type":"object","properties":{"device_code":{"type":"string"},"user_code":{"type":"string"},"verification_uri":{"type":"string"},"verification_uri_complete":{"type":"string"},"expires_in":{"type":"integer"},"interval":{"type":"integer"}}}}}},"4XX":{"description":"Client error — JSON body with a stable `error` code (e.g. not_found, not_authenticated, request_failed) and an optional `hint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Server error — JSON body with `error: \"server_error\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/auth/device/token":{"post":{"operationId":"pollDeviceToken","summary":"Poll for the device-authorization result (RFC 8628)","description":"Poll with the device_code until the user approves. Returns HTTP 400 with error=authorization_pending while waiting (also slow_down, expired_token, access_denied per RFC 8628); on success returns the access token to use as a Bearer token.","security":[{}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"device_code":{"type":"string"}},"required":["device_code"]}}}},"responses":{"200":{"description":"Access token once approved","content":{"application/json":{"schema":{"type":"object","properties":{"access_token":{"type":"string"},"token_type":{"type":"string"}}}}}},"400":{"description":"authorization_pending / slow_down / expired_token / access_denied"},"4XX":{"description":"Client error — JSON body with a stable `error` code (e.g. not_found, not_authenticated, request_failed) and an optional `hint`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Server error — JSON body with `error: \"server_error\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/mcp":{"post":{"operationId":"mcp","summary":"Hosted MCP server (JSON-RPC over Streamable HTTP)","description":"Model Context Protocol endpoint — canonical URL https://mcp.vakantio.de/mcp. Unauthenticated requests receive 401 with OAuth discovery metadata (RFC 9728); MCP clients handle the flow automatically.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"JSON-RPC 2.0 message"}}}},"responses":{"200":{"description":"JSON-RPC response"},"401":{"description":"Authentication required (triggers the client's OAuth flow)"}}}}}}