{"components":{"parameters":{"x_agency":{"description":"Target agency domain (`cname`). Obtain from `GET /api/agencies`.","example":"travelonly.com","in":"header","name":"x-agency","required":true,"schema":{"example":"travelonly.com","minLength":1,"type":"string"}}},"responses":{"bad_request":{"content":{"application/json":{"example":{"errors":{"detail":"X-Agency header is required"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Malformed request or invalid parameters."},"forbidden":{"content":{"application/json":{"example":{"errors":{"detail":"Insufficient scope","scope":"posts:write"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Authenticated but not authorized: missing OAuth scope (`insufficient_scope`)\nor no access to the requested agency.\n"},"no_content":{"description":"Success. No content returned."},"not_found":{"content":{"application/json":{"example":{"errors":{"detail":"Not Found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Resource not found or not accessible."},"unauthorized":{"content":{"application/json":{"example":{"errors":{"detail":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Missing, expired, or invalid Bearer token."},"unprocessable_entity":{"content":{"application/json":{"example":{"errors":{"price":["must be greater than 0"],"title":["can't be blank"]}},"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}},"description":"Validation failed. See `errors` for field-level messages."}},"schemas":{"Advisor":{"additionalProperties":false,"description":"Agency member eligible as a publish-as target (role > 1).","example":{"email":"madeline@example.com","name":"Madeline Advisor","role":2,"zid":"advisor-zid-uuid"},"properties":{"email":{"description":"Contact email (display / disambiguation only).","example":"madeline@example.com","format":"email","type":"string"},"name":{"description":"Display name.","example":"Madeline Advisor","type":"string"},"role":{"description":"TAT role integer (2 agent, 3 admin, 4 superadmin).","example":2,"type":"integer"},"zid":{"description":"TAT user zid — send as `author.zid` on write.","example":"advisor-zid-uuid","type":"string"}},"required":["zid","name","email","role"],"title":"Advisor","type":"object"},"AdvisorsResponse":{"description":"Paginated list of publish-as targets for the X-Agency agency.","example":{"data":[{"email":"madeline@example.com","name":"Madeline Advisor","role":2,"zid":"advisor-zid-uuid"}],"links":{"next":null,"prev":null,"self":"https://api.example.com/api/advisors?page=1"},"pagination":{"page_number":1,"page_size":30,"total_entries":1,"total_pages":1}},"properties":{"data":{"description":"Advisors sorted by name.","items":{"$ref":"#/components/schemas/Advisor"},"type":"array"},"links":{"$ref":"#/components/schemas/PaginationLinks"},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["data","pagination","links"],"title":"AdvisorsResponse","type":"object"},"AgenciesResponse":{"description":"List of manageable agencies.","example":{"data":[{"cms_group":{"id":3,"name":"North South Travel Group"},"cname":"northsouthtravel.com","name":"North South Travel"},{"cms_group":null,"cname":"travelonly.com","name":"Travel Only"}]},"properties":{"data":{"description":"Agencies sorted by name.","items":{"$ref":"#/components/schemas/Agency"},"type":"array"}},"required":["data"],"title":"AgenciesResponse","type":"object"},"Agency":{"description":"Tenant agency the user can manage.","example":{"cms_group":{"id":3,"name":"North South Travel Group"},"cname":"travelonly.com","name":"Travel Only"},"properties":{"cms_group":{"anyOf":[{"$ref":"#/components/schemas/CmsGroupSummary"}],"description":"Parent CMS group, or `null` if standalone.","nullable":true},"cname":{"description":"Domain name. Use as `X-Agency` header value.","example":"travelonly.com","type":"string"},"name":{"description":"Display name.","example":"Travel Only","type":"string"}},"required":["cname"],"title":"Agency","type":"object"},"Attachment":{"description":"Media file (image, video, or document) linked to a post or itinerary day.","example":{"id":10,"primary":true,"src":"https://cdn1.gttwl.net/attachments/global/cover.jpeg","title":"cover.jpeg","type":"photo"},"properties":{"id":{"description":"Unique identifier.","example":10,"format":"int64","readOnly":true,"type":"integer"},"primary":{"default":false,"description":"`true` if this is the post's cover/hero image.","type":"boolean"},"src":{"description":"CDN URL to the file.","example":"https://cdn1.gttwl.net/attachments/global/cover.jpeg","format":"uri","type":"string"},"title":{"description":"Original filename or caption.","example":"cover.jpeg","type":"string"},"type":{"description":"Media type: `photo`, `video`, or generic `file`.","enum":["photo","video","file"],"example":"photo","type":"string"}},"required":["src"],"title":"Attachment","type":"object"},"AuthorOverride":{"additionalProperties":false,"description":"Publish-as target. Send only on admin-grade create/update. Identifier is `zid`\n(same as JWT / `/auth/me` `sub`). Omit the entire `author` key for self-publish\n(create) or sticky authorship (update).\n","example":{"zid":"advisor-zid-uuid"},"properties":{"zid":{"description":"Target TAT user zid.","example":"advisor-zid-uuid","type":"string"}},"required":["zid"],"title":"AuthorOverride","type":"object"},"CategoriesResponse":{"description":"List of editorial categories.","example":{"data":[{"id":1,"permalink":"escorted-or-guided-tour","title":"Escorted or Guided Tour"},{"id":2,"permalink":"independent-travel","title":"Independent Travel"}]},"properties":{"data":{"description":"Categories with a title, sorted by title.","items":{"$ref":"#/components/schemas/Category"},"type":"array"}},"required":["data"],"title":"CategoriesResponse","type":"object"},"Category":{"description":"Editorial category for organizing posts. On write, pass `permalink` (preferred) or\n`id` — discover values via `GET /api/categories`. Do not look up by title.\n","example":{"id":1,"permalink":"escorted-or-guided-tour","title":"Escorted or Guided Tour"},"properties":{"id":{"description":"Unique identifier. Optional on write when `permalink` is provided.","example":1,"format":"int64","type":"integer"},"permalink":{"description":"Stable machine key (preferred on write). Default when omitted: `escorted-or-guided-tour`.","example":"escorted-or-guided-tour","type":"string"},"title":{"description":"Human-readable name.","example":"Escorted or Guided Tour","readOnly":true,"type":"string"}},"title":"Category","type":"object"},"CmsGroupSummary":{"description":"CMS group that agencies can belong to.","example":{"id":3,"name":"North South Travel Group"},"properties":{"id":{"description":"Unique identifier.","example":3,"format":"int64","readOnly":true,"type":"integer"},"name":{"description":"Display name.","example":"North South Travel Group","type":"string"}},"required":["id","name"],"title":"CmsGroupSummary","type":"object"},"ErrorDetail":{"description":"Error detail object.","properties":{"detail":{"description":"Human-readable message.","example":"Not Found","type":"string"}},"required":["detail"],"title":"ErrorDetail","type":"object"},"ErrorResponse":{"description":"Standard error envelope for 4xx/5xx responses.","example":{"errors":{"detail":"Not Found"}},"properties":{"errors":{"allOf":[{"$ref":"#/components/schemas/ErrorDetail"}],"description":"Error details."}},"required":["errors"],"title":"ErrorResponse","type":"object"},"Faq":{"additionalProperties":false,"description":"FAQ entry for a post. Ordered by array index on write; position is server-assigned.","example":{"answer":"Late October through early November.","question":"When is the best time to go?"},"properties":{"answer":{"description":"FAQ answer (plain text or HTML).","example":"Late October through early November.","type":"string"},"question":{"description":"FAQ question.","example":"When is the best time to go?","type":"string"}},"required":["question","answer"],"title":"Faq","type":"object"},"Itinerary":{"additionalProperties":false,"description":"Single day within a trip itinerary.","example":{"content":"<p>Welcome to Mexico City. Transfer to hotel and free time.</p>","id":1,"line":0,"place":{"country_code":"MX","id":42,"title":"Mexico City"},"title":"Arrival in Mexico City"},"properties":{"attachments":{"description":"Media for this day.","items":{"$ref":"#/components/schemas/Attachment"},"type":"array"},"content":{"description":"Day description (HTML or plain text).","example":"<p>Welcome to Mexico City. Transfer to hotel.</p>","type":"string"},"created_at":{"description":"Creation timestamp.","format":"date-time","readOnly":true,"type":"string"},"end_date":{"description":"End datetime (ISO 8601).","format":"date-time","type":"string"},"href":{"description":"Canonical URL.","format":"uri","readOnly":true,"type":"string"},"id":{"description":"Unique identifier.","example":1,"format":"int64","readOnly":true,"type":"integer"},"line":{"default":0,"description":"Day index (0-based). Day 1 = `0`.","example":0,"minimum":0,"type":"integer"},"place":{"$ref":"#/components/schemas/Place"},"price":{"description":"Optional day price.","example":4059.0,"format":"double","type":"number"},"start_date":{"description":"Start datetime (ISO 8601).","format":"date-time","type":"string"},"title":{"description":"Day heading.","example":"Arrival in Mexico City","maxLength":1024,"type":"string"},"updated_at":{"description":"Last update timestamp.","format":"date-time","readOnly":true,"type":"string"}},"required":["title"],"title":"Itinerary","type":"object"},"ItineraryAttachmentUploadRequest":{"description":"Multipart upload for itinerary line attachments.","example":{"uploads":["<binary file 1>"]},"properties":{"uploads":{"description":"One or more files.","items":{"format":"binary","type":"string"},"minItems":1,"type":"array"}},"required":["uploads"],"title":"ItineraryAttachmentUploadRequest","type":"object"},"ItineraryRequest":{"description":"Create or update an itinerary line.","example":{"itinerary":{"content":"<p>Welcome to Mexico City.</p>","line":0,"place":{"title":"Mexico City"},"title":"Arrival in Mexico City"}},"properties":{"itinerary":{"properties":{"content":{"description":"Day description (HTML or plain text).","type":"string"},"end_date":{"description":"End date (YYYY-MM-DD).","format":"date","type":"string"},"line":{"default":0,"description":"Day index (0-based).","minimum":0,"type":"integer"},"place":{"$ref":"#/components/schemas/Place"},"price":{"description":"Optional day price.","format":"double","type":"number"},"start_date":{"description":"Start date (YYYY-MM-DD).","format":"date","type":"string"},"title":{"description":"Day heading.","example":"Arrival in Mexico City","maxLength":1024,"type":"string"}},"type":"object"}},"required":["itinerary"],"title":"ItineraryRequest","type":"object"},"ItineraryResponse":{"description":"Single itinerary line response.","properties":{"data":{"allOf":[{"$ref":"#/components/schemas/Itinerary"}],"description":"Itinerary line resource."}},"required":["data"],"title":"ItineraryResponse","type":"object"},"Pagination":{"description":"Pagination metadata.","properties":{"page_number":{"description":"Current page (1-based).","example":1,"minimum":1,"type":"integer"},"page_size":{"description":"Items per page.","example":30,"minimum":1,"type":"integer"},"total_entries":{"description":"Total matching items.","example":142,"minimum":0,"type":"integer"},"total_pages":{"description":"Total pages available.","example":5,"minimum":0,"type":"integer"}},"required":["page_number","page_size","total_pages","total_entries"],"title":"Pagination","type":"object"},"PaginationLinks":{"description":"Navigation links for paginated results.","properties":{"next":{"description":"Next page URL, or `null` on last page.","format":"uri","nullable":true,"type":"string"},"prev":{"description":"Previous page URL, or `null` on first page.","format":"uri","nullable":true,"type":"string"},"self":{"description":"Current page URL.","format":"uri","type":"string"}},"required":["self"],"title":"PaginationLinks","type":"object"},"Place":{"description":"Geographic location associated with a post or itinerary day.","example":{"country_code":"MX","id":42,"title":"Mexico City"},"properties":{"country_code":{"description":"ISO 3166-1 alpha-2 country code.","example":"MX","pattern":"^[A-Z]{2}$","readOnly":true,"type":"string"},"id":{"description":"Unique identifier.","example":42,"format":"int64","readOnly":true,"type":"integer"},"title":{"description":"Display name (city, region, or landmark).","example":"Mexico City","type":"string"}},"required":["title"],"title":"Place","type":"object"},"Post":{"additionalProperties":false,"description":"Complete post resource with content, pricing, itinerary, and attachments.","example":{"active":true,"attachments":[],"category":{"id":1,"permalink":"escorted-or-guided-tour","title":"Escorted or Guided Tour"},"currency":"USD","faqs":[],"href":"https://api.example.com/api/posts/1","id":1,"itinerary":[],"language":"en","permalink":"/blog/mexico-city-to-oaxaca-celebrate-day-of-the-dead","place":{"country_code":"MX","id":42,"title":"Mexico City"},"price":10149.5,"src":"partner_app","src_id":"ext_story_abc123","summary":"An 8-day journey from Mexico City into the golden marigold fields of Atlixco.","tags":"gadventure,escorted_tour,classic","title":"Mexico City to Oaxaca: Celebrate Day of the Dead","type":{"id":1,"key":"product","name":"Products"}},"properties":{"active":{"default":false,"description":"`true` = publicly visible.","type":"boolean"},"attachments":{"description":"Media files (read-only). Upload via `/api/posts/{id}/attachments`.","items":{"$ref":"#/components/schemas/Attachment"},"readOnly":true,"type":"array"},"author":{"allOf":[{"$ref":"#/components/schemas/AuthorOverride"}],"description":"On write: send `{\"zid\": \"…\"}` to publish-as (admin-grade only; 4-check enforced).\nOmit on create for token-user authorship; omit on update to keep sticky author.\nOn read: response uses the attributed user object including `zid`.\n"},"category":{"$ref":"#/components/schemas/Category"},"content":{"description":"Full post body (HTML).","type":"string"},"created_at":{"description":"Creation timestamp (ISO 8601).","format":"date-time","readOnly":true,"type":"string"},"currency":{"default":"USD","description":"ISO 4217 currency code.","enum":["USD","CAD","EUR"],"example":"USD","maxLength":5,"type":"string"},"data":{"description":"SEO meta persisted to `posts.data` (hstore).","example":{"meta_description":"An 8-day journey…","meta_keywords":"oaxaca,day of the dead","meta_title":"Day of the Dead in Oaxaca"},"properties":{"meta_description":{"description":"SEO description.","type":"string"},"meta_keywords":{"description":"SEO keywords.","type":"string"},"meta_title":{"description":"SEO title.","type":"string"}},"type":"object"},"faqs":{"description":"Ordered FAQ set. On create/update, when present, **replaces** the full set\n(cap 50). Omit the key to leave existing FAQs unchanged.\n","items":{"$ref":"#/components/schemas/Faq"},"maxItems":50,"type":"array"},"href":{"description":"Canonical URL.","example":"https://api.example.com/api/posts/1","format":"uri","readOnly":true,"type":"string"},"id":{"description":"Unique identifier.","example":1,"format":"int64","readOnly":true,"type":"integer"},"itinerary":{"description":"Day-by-day entries (read-only).","items":{"$ref":"#/components/schemas/Itinerary"},"readOnly":true,"type":"array"},"language":{"description":"Content language code (stored as `lang`).","example":"en","type":"string"},"permalink":{"description":"URL-safe slug (auto-generated).","example":"/blog/mexico-city-to-oaxaca-celebrate-day-of-the-dead","maxLength":1024,"type":"string"},"place":{"$ref":"#/components/schemas/Place"},"price":{"description":"Base price.","example":10149.5,"format":"double","type":"number"},"price_info":{"description":"Pricing notes or T&C (HTML).","type":"string"},"src":{"description":"Provenance source system identifier (your product or integration name).","example":"partner_app","type":"string"},"src_id":{"description":"External record id in the source system (for sync and support).","example":"ext_story_abc123","type":"string"},"summary":{"description":"Short teaser for listing views.","example":"An 8-day journey from Mexico City into the golden marigold fields of Atlixco.","maxLength":1024,"type":"string"},"tags":{"description":"Comma-separated lowercase tags.","example":"gadventure,escorted_tour,classic","type":"string"},"title":{"description":"Headline (6–72 characters).","example":"Mexico City to Oaxaca: Celebrate Day of the Dead","maxLength":72,"minLength":6,"type":"string"},"travel_date":{"description":"Departure date (ISO 8601).","format":"date-time","type":"string"},"travel_date_end":{"description":"Return date (ISO 8601).","format":"date-time","type":"string"},"type":{"$ref":"#/components/schemas/Type"},"updated_at":{"description":"Last update timestamp (ISO 8601).","format":"date-time","readOnly":true,"type":"string"}},"title":"Post","type":"object"},"PostAttachmentUploadRequest":{"description":"Multipart upload request. Use `uploads` field (repeat for multiple files).\nOptional `primary=true` designates the first file as the post hero (re-designates\neven when a hero already exists). Preferred hero size: 1200×630 (not enforced).\n","example":{"primary":true,"uploads":["<binary file 1>","<binary file 2>"]},"properties":{"primary":{"default":false,"description":"When `true`, the first upload becomes the post cover/hero image.","type":"boolean"},"uploads":{"description":"One or more files. Repeat the `uploads` field for each file.","items":{"format":"binary","type":"string"},"minItems":1,"type":"array"}},"required":["uploads"],"title":"PostAttachmentUploadRequest","type":"object"},"PostRequest":{"description":"Create or update a post. Use JSON only; upload media via `/api/posts/{id}/attachments`.\nWhen `type` is omitted on create, defaults to `blog`. When `category` is omitted,\ndefaults to permalink `escorted-or-guided-tour`. Pass `category: {permalink}` or\n`{id}`. Optional `author: {\"zid\"}` for admin-grade publish-as (omit = self on create,\nsticky on update).\n","example":{"post":{"active":true,"author":{"zid":"advisor-zid-uuid"},"content":"<p>First-person trip story…</p>","data":{"meta_description":"An 8-day journey…","meta_keywords":"oaxaca,day of the dead","meta_title":"Day of the Dead in Oaxaca"},"faqs":[{"answer":"Late October through early November.","question":"When is the best time to go?"}],"language":"en","place":{"title":"Mexico City"},"src":"partner_app","src_id":"ext_story_abc123","summary":"An 8-day journey from Mexico City into the golden marigold fields of Atlixco.","title":"Mexico City to Oaxaca: Celebrate Day of the Dead"}},"properties":{"post":{"anyOf":[{"$ref":"#/components/schemas/Post"}],"description":"Post attributes. Read-only fields (`id`, `href`, `itinerary`, `attachments`) are ignored on write."}},"required":["post"],"title":"PostRequest","type":"object"},"PostResponse":{"description":"Single post response.","example":{"data":{"active":true,"href":"https://api.example.com/api/posts/1","id":1,"title":"Mexico City to Oaxaca: Celebrate Day of the Dead"}},"properties":{"data":{"allOf":[{"$ref":"#/components/schemas/Post"}],"description":"Post resource."}},"required":["data"],"title":"PostResponse","type":"object"},"PostSummary":{"description":"Condensed post for list views. Omits `content`, `itinerary`, and pricing.","properties":{"active":{"description":"Visibility status.","type":"boolean"},"attachments":{"description":"Media files.","items":{"$ref":"#/components/schemas/Attachment"},"type":"array"},"author":{"$ref":"#/components/schemas/User"},"category":{"$ref":"#/components/schemas/Category"},"created_at":{"description":"Creation timestamp.","format":"date-time","readOnly":true,"type":"string"},"href":{"description":"Canonical URL.","format":"uri","readOnly":true,"type":"string"},"id":{"description":"Unique identifier.","format":"int64","readOnly":true,"type":"integer"},"place":{"$ref":"#/components/schemas/Place"},"summary":{"description":"Short teaser.","type":"string"},"title":{"description":"Headline.","type":"string"},"type":{"$ref":"#/components/schemas/Type"},"updated_at":{"description":"Last update timestamp.","format":"date-time","readOnly":true,"type":"string"}},"required":["title"],"title":"PostSummary","type":"object"},"PostsResponse":{"description":"Paginated list of posts.","example":{"data":[{"active":true,"href":"https://api.example.com/api/posts/1","id":1,"title":"Mexico City to Oaxaca: Celebrate Day of the Dead"}],"links":{"next":null,"prev":null,"self":"https://api.example.com/api/posts?page=1"},"pagination":{"page_number":1,"page_size":30,"total_entries":1,"total_pages":1}},"properties":{"data":{"description":"Posts for the current page.","items":{"$ref":"#/components/schemas/PostSummary"},"type":"array"},"links":{"$ref":"#/components/schemas/PaginationLinks"},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["data","pagination","links"],"title":"PostsResponse","type":"object"},"Type":{"description":"Post type classification.","example":{"id":1,"key":"product","name":"Products"},"properties":{"id":{"description":"Internal identifier (read-only).","example":1,"format":"int64","readOnly":true,"type":"integer"},"key":{"description":"Machine-readable key. Pass only `key` when creating/updating a post.","enum":["tip","question","product","video","photo","blog","place","testimonial","book","offer"],"example":"product","type":"string"},"name":{"description":"Display name.","example":"Products","readOnly":true,"type":"string"}},"required":["key"],"title":"Type","type":"object"},"User":{"additionalProperties":false,"description":"Post author (populated from the JWT on write). Response includes `zid` — the same\n        value as JWT / OAuth userinfo `sub` — so clients can confirm attribution.","example":{"email":"somchai@example.com","id":7,"name":"Somchai Jaidee","organization":"Bangkok Travel Co.","title":"Senior Travel Consultant","zid":"user-zid-uuid"},"properties":{"created_at":{"description":"Creation timestamp (ISO 8601).","format":"date-time","readOnly":true,"type":"string"},"email":{"description":"Contact email.","example":"somchai@example.com","format":"email","type":"string"},"href":{"description":"Canonical URL.","format":"uri","readOnly":true,"type":"string"},"id":{"description":"Internal numeric identifier.","example":7,"format":"int64","readOnly":true,"type":"integer"},"name":{"description":"Full name.","example":"Somchai Jaidee","type":"string"},"organization":{"description":"Agency name.","example":"Bangkok Travel Co.","type":"string"},"title":{"description":"Job title.","example":"Senior Travel Consultant","type":"string"},"updated_at":{"description":"Last update timestamp (ISO 8601).","format":"date-time","readOnly":true,"type":"string"},"zid":{"description":"TAT user zid — same value as JWT/`/auth/me` `sub`.","example":"user-zid-uuid","readOnly":true,"type":"string"}},"required":["name","email"],"title":"User","type":"object"},"ValidationErrorResponse":{"description":"Validation error envelope. Keys = field names; values = error message arrays.","example":{"errors":{"price":["must be greater than 0"],"title":["can't be blank"]}},"properties":{"errors":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"description":"Field-level error messages.","example":{"price":["must be greater than 0"],"title":["can't be blank"]},"type":"object"}},"required":["errors"],"title":"ValidationErrorResponse","type":"object"}},"securitySchemes":{"bearerAuth":{"bearerFormat":"JWT","description":"RS256 access token from TAT OAuth (`https://auth.gttwl.net`).\nObtain via authorization code + PKCE, then refresh with `grant_type=refresh_token`.\n","scheme":"bearer","type":"http"}}},"externalDocs":{"description":"OAuth & connector integration guide","url":"/docs/oauth"},"info":{"contact":{"email":"support@travelagencytribes.com","name":"Travel Agency Tribes Support","url":"https://travelagencytribes.com"},"description":"Multi-tenant REST API for travel-agency content: agencies, posts (blog, products,\nitineraries), media attachments, SEO metadata, and FAQs.\n\nDesigned for **first-party apps and third-party connectors**. Any OAuth client with\nthe appropriate scopes can authenticate users and publish or manage content for\nagencies they are authorized to access.\n\n---\n\n## Base URL (set this explicitly)\n\n| Environment | Base URL |\n|-------------|---------|\n| **Production** | `https://api-edge.gttwl.net` |\n| Local dev | `http://localhost:4040` |\n\nOpenAPI `servers` and live docs resolve from this host. Do **not** use\n`https://api.gttwl2.com` — that is a different legacy service (`org_api`) and does\n**not** host `/api/agencies`, `/api/posts`, or `/api/advisors`.\n\n---\n\n## Quick start (agents & integrators)\n\n1. Register a confidential OAuth client on the [TAT OAuth](https://auth.gttwl.net) server\n   (`authorization_code` + PKCE `S256`). Typical scopes:\n   `openid`, `profile`, `agencies:read`, `categories:read`, `posts:read`,\n   `posts:write`.\n2. Complete the OAuth code flow; store access + refresh tokens securely (server-side\n   if publishing must work outside a browser session).\n3. Verify the link with `GET https://auth.gttwl.net/auth/me` (`sub` is the user `zid`).\n4. Call `GET https://api-edge.gttwl.net/api/agencies` (Bearer, **no** `X-Agency`)\n   and select a `cname`.\n5. Optionally call `GET /api/categories` for category `permalink` values.\n6. Call agency-scoped routes with `Authorization: Bearer …` and `X-Agency: <cname>`.\n\nHuman-readable guide: [OAuth & connector integration](/docs/oauth).\nMachine-readable contract: this document (`GET /api/openapi`).\n\n---\n\n## Authentication\n\nAll `/api/*` routes require a Bearer JWT issued by TAT OAuth. This API introspects\ntokens at `https://auth.gttwl.net/auth/introspect` — it does not maintain its own JWKS or\nclient registry.\n\n```http\nAuthorization: Bearer <access_token>\n```\n\n| Token | Lifetime (confidential clients) | Notes |\n|-------|----------------------------------|-------|\n| Access | 15 minutes | RS256 JWT |\n| Refresh | 365 days | Rotates on use; reuse of a revoked refresh chain-revokes |\n\nPKCE is **required** (`S256` only; `plain` rejected).\n\n---\n\n## OAuth scopes (resource server)\n\nEvery protected route requires the matching access-token scope (AND semantics).\nMissing scope → **403** with `WWW-Authenticate: Bearer error=\"insufficient_scope\"`.\n\n| Scope | Routes |\n|-------|--------|\n| `agencies:read` | `GET /api/agencies` |\n| `categories:read` | `GET /api/categories` |\n| `posts:read` | `GET /api/posts`, `GET /api/posts/{id}` |\n| `posts:write` | Post create/update/delete and nested attachments/itinerary |\n\nEmpty-scope tokens are rejected for API calls. Connector clients should request:\n`openid profile agencies:read categories:read posts:read posts:write`.\n\n---\n\n## Tenancy (`X-Agency`)\n\nAgency-scoped routes require the target agency domain:\n\n```http\nX-Agency: travelonly.com\n```\n\nDiscover valid values via `GET /api/agencies`. Missing header → **400**. Valid token\nbut no access to that agency → **403**.\n\n---\n\n## Role-based access\n\nEffective role is resolved from the JWT **and** the target agency:\n\n| Effective role | JWT / membership | Access |\n|----------------|------------------|--------|\n| `superadmin` | role `4` | Any agency, all posts |\n| `cms_group_admin` | role `2`/`3` + CMS group match | All posts in group agencies |\n| `admin` | role `3`, home agency | All posts in that agency |\n| `agent` | role `2`, home agency | Own posts only |\n\n---\n\n## Response envelope\n\nSuccessful resource responses use a `data` wrapper (never a bare top-level `id`):\n\n```json\n{ \"data\": { \"id\": 12345, \"permalink\": \"/blog/...\", \"active\": true, \"place\": null } }\n```\n\nList endpoints also return `pagination` and `links`. Treat `place: null` as\nunresolved place lookup (publish still succeeds).\n\n---\n\n## Recommended publish sequence (content connectors)\n\nThe API does **not** rewrite HTML. Clients that publish rich content should:\n\n1. **`POST /api/posts`** — title, summary, prose `content`, optional `language`,\n   `data` (SEO), `faqs`, `src` / `src_id`, `place: {title}`. Omit `type` to default\n   to `blog`; omit `category` to default to permalink `escorted-or-guided-tour`\n   (or pass `category: {permalink}` from `GET /api/categories`).\n2. **`POST /api/posts/{id}/attachments`** — multipart `uploads`; set `primary=true`\n   on the hero batch. Preferred hero size **1200×630** (guidance only). A later\n   `primary=true` upload re-designates the cover without deleting prior files.\n3. **`PUT /api/posts/{id}`** — final HTML `content` with CDN `<img>` URLs from step 2.\n   When `faqs` is present it **replaces** the full FAQ set (cap 50); omit the key\n   to leave FAQs unchanged.\n\nAuthorship: omit `author` on create to attribute the post to the token user.\nAdmin-grade callers may send `author: {\"zid\": \"…\"}` (4-check enforced). On update,\nomit `author` to keep the existing author (sticky); send `author.zid` to re-attribute.\nDiscover targets via `GET /api/advisors`.\n\n---\n\n## Provenance\n\nOptional write fields for support and idempotent sync:\n\n| Field | Purpose |\n|-------|---------|\n| `src` | Source system identifier (e.g. your product name) |\n| `src_id` | External record id in that system |\n\n---\n\n## Content types\n\n| Content-Type | Use |\n|--------------|-----|\n| `application/json` | Reads and JSON create/update bodies |\n| `multipart/form-data` | `POST …/attachments` file uploads |\n\n---\n\n## OpenAPI resources\n\n| Resource | URL |\n|----------|-----|\n| This JSON spec | `GET /api/openapi` |\n| Interactive UI | `/` (Swagger UI) |\n| OAuth guide | `/docs/oauth` |\n","title":"Tatex API","version":"1.0.0"},"openapi":"3.0.0","paths":{"/api/advisors":{"get":{"callbacks":{},"description":"Paginated list of publish-as targets for the `X-Agency` agency: users with\n**role > 1** (agents, admins, and above) including their `zid`.\n\n**Caller roles:** `admin` / `cms_group_admin` / `superadmin` only. Agents receive **403**.\n\nUsed by Amplify to populate the author picker for unlinked advisors (v2 publish-as).\n","operationId":"TatexApiWeb.AdvisorController.index","parameters":[{"$ref":"#/components/parameters/x_agency"},{"description":"Page number (1-based).","in":"query","name":"page","required":false,"schema":{"default":1,"minimum":1,"type":"integer"}},{"description":"Items per page (max 100).","in":"query","name":"page_size","required":false,"schema":{"default":30,"maximum":100,"minimum":1,"type":"integer"}},{"description":"Search by name or email (case-insensitive).","in":"query","name":"q","required":false,"schema":{"example":"madeline","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvisorsResponse"}}},"description":"Advisor list."},"400":{"$ref":"#/components/responses/bad_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"}},"summary":"List agency advisors","tags":["advisors"]}},"/api/agencies":{"get":{"callbacks":{},"description":"Returns agencies the user can manage, sorted by name.\n\n**Access by role:**\n- `superadmin` — all agencies\n- `cms_group_admin` — agencies in their CMS groups\n- `admin` / `agent` — their home agency only\n\nUse a returned `cname` as `X-Agency` on other endpoints.\n\n> **Note:** This endpoint does **not** require `X-Agency`.\n","operationId":"TatexApiWeb.AgencyController.index","parameters":[{"description":"Search by name, domain, or CMS group (case-insensitive).","in":"query","name":"q","required":false,"schema":{"example":"travel","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgenciesResponse"}}},"description":"Agency list."},"401":{"$ref":"#/components/responses/unauthorized"}},"summary":"List agencies","tags":["agencies"]}},"/api/categories":{"get":{"callbacks":{},"description":"Returns all editorial categories (global taxonomy), sorted by title.\n\nUse a returned `permalink` when creating or updating posts:\n\n```json\n{ \"post\": { \"category\": { \"permalink\": \"escorted-or-guided-tour\" } } }\n```\n\nWhen `category` is omitted on create/update, the API defaults to\n`escorted-or-guided-tour`.\n\n> **Note:** This endpoint does **not** require `X-Agency`.\n","operationId":"TatexApiWeb.CategoryController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CategoriesResponse"}}},"description":"Category list."},"401":{"$ref":"#/components/responses/unauthorized"}},"summary":"List categories","tags":["categories"]}},"/api/posts":{"get":{"callbacks":{},"description":"Paginated list of posts in the target agency.\n\n**Access by role:**\n- `superadmin` / `cms_group_admin` / `admin` — all agency posts\n- `agent` — own posts only\n\nUse query parameters to filter and paginate results.\n","operationId":"TatexApiWeb.PostController.index","parameters":[{"$ref":"#/components/parameters/x_agency"},{"description":"Page number (1-based).","in":"query","name":"page","required":false,"schema":{"default":1,"minimum":1,"type":"integer"}},{"description":"Items per page (max 100).","in":"query","name":"page_size","required":false,"schema":{"default":30,"maximum":100,"minimum":1,"type":"integer"}},{"description":"Filter by type key.","in":"query","name":"type","required":false,"schema":{"enum":["tip","question","product","video","photo","blog","place","testimonial","book","offer"],"example":"product","type":"string"}},{"description":"Filter by visibility. Omit for all.","in":"query","name":"active","required":false,"schema":{"type":"boolean"}},{"description":"Filter by author ID.","in":"query","name":"author_id","required":false,"schema":{"format":"int64","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostsResponse"}}},"description":"Post list."},"400":{"$ref":"#/components/responses/bad_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"}},"summary":"List posts","tags":["posts"]},"post":{"callbacks":{},"description":"Creates a new post in the target agency.\n\n**Auto-populated:** `permalink`, `zid`, `user_id`, and `agency_id` are derived from the\nJWT and `X-Agency`. When `type` is omitted, defaults to `blog`. When `category` is\nomitted, defaults to permalink `escorted-or-guided-tour` (\"Escorted or Guided Tour\").\nPass `category: {permalink}` (preferred) or `category: {id}` — see `GET /api/categories`.\n\n**Recommended connector sequence:** create prose content → upload attachments with\n`primary=true` for the hero → `PUT` with finished HTML containing CDN `<img>` URLs.\nThis API does not rewrite HTML.\n\n**FAQs:** when `faqs` is present, the ordered set is stored (cap 50). Omit the key when\nyou have none.\n\n**Authorship:** omit `author` to attribute the post to the token user. Admin-grade\ncallers (`admin` / `cms_group_admin` / `superadmin`) may send `author: {\"zid\": \"…\"}`\nto publish-as another agency member with role > 1 (4-check enforced).\n\n**Media:** upload via `POST /api/posts/{post_id}/attachments` after creation.\nPreferred hero dimensions: **1200×630** (guidance only; not rejected).\n","operationId":"TatexApiWeb.PostController.create","parameters":[{"$ref":"#/components/parameters/x_agency"}],"requestBody":{"content":{"application/json":{"example":{"post":{"active":true,"content":"<p>First-person trip story…</p>","data":{"meta_description":"An 8-day journey…","meta_keywords":"oaxaca,day of the dead","meta_title":"Day of the Dead in Oaxaca"},"faqs":[{"answer":"Late October through early November.","question":"When is the best time to go?"}],"language":"en","place":{"title":"Mexico City"},"src":"partner_app","src_id":"ext_story_abc123","summary":"An 8-day journey from Mexico City into the marigold fields of Atlixco.","title":"Mexico City to Oaxaca: Celebrate Day of the Dead"}},"schema":{"$ref":"#/components/schemas/PostRequest"}}},"description":"Post attributes.","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostResponse"}}},"description":"Created post."},"400":{"$ref":"#/components/responses/bad_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"422":{"$ref":"#/components/responses/unprocessable_entity"}},"summary":"Create a post","tags":["posts"]}},"/api/posts/{id}":{"delete":{"callbacks":{},"description":"Permanently deletes a post. **Irreversible.**\n\n**Access:** agents can only delete their own posts (others return `404`).\n","operationId":"TatexApiWeb.PostController.delete","parameters":[{"$ref":"#/components/parameters/x_agency"},{"description":"Post ID.","in":"path","name":"id","required":true,"schema":{"minimum":1,"type":"integer"}}],"responses":{"204":{"$ref":"#/components/responses/no_content"},"400":{"$ref":"#/components/responses/bad_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"}},"summary":"Delete a post","tags":["posts"]},"get":{"callbacks":{},"description":"Full post with content, pricing, itinerary, and attachments.\n\n**Access:** agents can only view their own posts (others return `404`).\n","operationId":"TatexApiWeb.PostController.show","parameters":[{"$ref":"#/components/parameters/x_agency"},{"description":"Post ID.","in":"path","name":"id","required":true,"schema":{"minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostResponse"}}},"description":"Post details."},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"}},"summary":"Get a post","tags":["posts"]},"patch":{"callbacks":{},"description":"Updates supplied fields. `PUT` and `PATCH` behave identically for field merges.\nWhen `faqs` is present it **fully replaces** the FAQ set (no append).\n\nWhen `category` is omitted, defaults to permalink `escorted-or-guided-tour`.\nPass `category: {permalink}` or `category: {id}` to select another category.\n\n**Access:** agents can only update their own posts (others return `404`).\n\n**Authorship:** omit `author` to keep the existing author (sticky). Admin-grade\ncallers may send `author: {\"zid\": \"…\"}` to re-attribute (4-check enforced).\nRead-only on write: `permalink`, `itinerary`, and `attachments`.\n","operationId":"TatexApiWeb.PostController.update (2)","parameters":[{"$ref":"#/components/parameters/x_agency"},{"description":"Post ID.","in":"path","name":"id","required":true,"schema":{"minimum":1,"type":"integer"}}],"requestBody":{"content":{"application/json":{"example":{"post":{"active":true,"content":"<p>First-person trip story…</p>","data":{"meta_description":"An 8-day journey…","meta_keywords":"oaxaca,day of the dead","meta_title":"Day of the Dead in Oaxaca"},"faqs":[{"answer":"Late October through early November.","question":"When is the best time to go?"}],"language":"en","place":{"title":"Mexico City"},"src":"partner_app","src_id":"ext_story_abc123","summary":"An 8-day journey from Mexico City into the marigold fields of Atlixco.","title":"Mexico City to Oaxaca: Celebrate Day of the Dead"}},"schema":{"$ref":"#/components/schemas/PostRequest"}}},"description":"Post attributes.","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostResponse"}}},"description":"Updated post."},"400":{"$ref":"#/components/responses/bad_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"},"422":{"$ref":"#/components/responses/unprocessable_entity"}},"summary":"Update a post","tags":["posts"]},"put":{"callbacks":{},"description":"Updates supplied fields. `PUT` and `PATCH` behave identically for field merges.\nWhen `faqs` is present it **fully replaces** the FAQ set (no append).\n\nWhen `category` is omitted, defaults to permalink `escorted-or-guided-tour`.\nPass `category: {permalink}` or `category: {id}` to select another category.\n\n**Access:** agents can only update their own posts (others return `404`).\n\n**Authorship:** omit `author` to keep the existing author (sticky). Admin-grade\ncallers may send `author: {\"zid\": \"…\"}` to re-attribute (4-check enforced).\nRead-only on write: `permalink`, `itinerary`, and `attachments`.\n","operationId":"TatexApiWeb.PostController.update","parameters":[{"$ref":"#/components/parameters/x_agency"},{"description":"Post ID.","in":"path","name":"id","required":true,"schema":{"minimum":1,"type":"integer"}}],"requestBody":{"content":{"application/json":{"example":{"post":{"active":true,"content":"<p>First-person trip story…</p>","data":{"meta_description":"An 8-day journey…","meta_keywords":"oaxaca,day of the dead","meta_title":"Day of the Dead in Oaxaca"},"faqs":[{"answer":"Late October through early November.","question":"When is the best time to go?"}],"language":"en","place":{"title":"Mexico City"},"src":"partner_app","src_id":"ext_story_abc123","summary":"An 8-day journey from Mexico City into the marigold fields of Atlixco.","title":"Mexico City to Oaxaca: Celebrate Day of the Dead"}},"schema":{"$ref":"#/components/schemas/PostRequest"}}},"description":"Post attributes.","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostResponse"}}},"description":"Updated post."},"400":{"$ref":"#/components/responses/bad_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"},"422":{"$ref":"#/components/responses/unprocessable_entity"}},"summary":"Update a post","tags":["posts"]}},"/api/posts/{post_id}/attachments":{"post":{"callbacks":{},"description":"Uploads one or more files and links them to a post.\n\n**Request:** `multipart/form-data` with at least one `uploads` field.\nRepeat the field for multiple files.\n\n**Hero / primary:** pass `primary=true` to designate the first file in this\nrequest as the post cover image. A fresh `primary=true` upload **re-designates**\nthe hero even when the post already has an `attachment_id` (swap heroes by\nuploading, not by deleting).\n\nPreferred hero dimensions: **1200×630** (guidance only; uploads are not rejected\nfor other sizes).\n\n**Max size:** ~1 GB per request.\n","operationId":"TatexApiWeb.PostAttachmentController.create","parameters":[{"$ref":"#/components/parameters/x_agency"},{"description":"Post ID.","in":"path","name":"post_id","required":true,"schema":{"minimum":1,"type":"integer"}}],"requestBody":{"content":{"multipart/form-data":{"example":{"primary":true,"uploads":["<binary file 1>","<binary file 2>"]},"schema":{"$ref":"#/components/schemas/PostAttachmentUploadRequest"}}},"description":"File uploads.","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostResponse"}}},"description":"Post with new attachments."},"400":{"$ref":"#/components/responses/bad_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"}},"summary":"Upload attachments","tags":["posts"]}},"/api/posts/{post_id}/attachments/{id}":{"delete":{"callbacks":{},"description":"Removes an attachment from a post. Clears the primary reference if this was the cover image.\n","operationId":"TatexApiWeb.PostAttachmentController.delete","parameters":[{"$ref":"#/components/parameters/x_agency"},{"description":"Post ID.","in":"path","name":"post_id","required":true,"schema":{"minimum":1,"type":"integer"}},{"description":"Attachment ID.","in":"path","name":"id","required":true,"schema":{"minimum":1,"type":"integer"}}],"responses":{"204":{"$ref":"#/components/responses/no_content"},"400":{"$ref":"#/components/responses/bad_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"}},"summary":"Delete an attachment","tags":["posts"]}},"/api/posts/{post_id}/itinerary":{"post":{"callbacks":{},"description":"Creates a new itinerary line (day) for a post.","operationId":"TatexApiWeb.PostItineraryController.create","parameters":[{"$ref":"#/components/parameters/x_agency"},{"description":"Post ID.","in":"path","name":"post_id","required":true,"schema":{"minimum":1,"type":"integer"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItineraryRequest"}}},"description":"Itinerary line attributes.","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItineraryResponse"}}},"description":"Created itinerary line."},"400":{"$ref":"#/components/responses/bad_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"},"422":{"$ref":"#/components/responses/unprocessable_entity"}},"summary":"Add itinerary line","tags":["posts"]}},"/api/posts/{post_id}/itinerary/{id}":{"delete":{"callbacks":{},"description":"Removes an itinerary line and its attachments from a post.","operationId":"TatexApiWeb.PostItineraryController.delete","parameters":[{"$ref":"#/components/parameters/x_agency"},{"description":"Post ID.","in":"path","name":"post_id","required":true,"schema":{"minimum":1,"type":"integer"}},{"description":"Itinerary line ID.","in":"path","name":"id","required":true,"schema":{"minimum":1,"type":"integer"}}],"responses":{"204":{"$ref":"#/components/responses/no_content"},"400":{"$ref":"#/components/responses/bad_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"}},"summary":"Delete itinerary line","tags":["posts"]},"patch":{"callbacks":{},"description":"Updates an existing itinerary line.","operationId":"TatexApiWeb.PostItineraryController.update (2)","parameters":[{"$ref":"#/components/parameters/x_agency"},{"description":"Post ID.","in":"path","name":"post_id","required":true,"schema":{"minimum":1,"type":"integer"}},{"description":"Itinerary line ID.","in":"path","name":"id","required":true,"schema":{"minimum":1,"type":"integer"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItineraryRequest"}}},"description":"Itinerary line attributes.","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItineraryResponse"}}},"description":"Updated itinerary line."},"400":{"$ref":"#/components/responses/bad_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"},"422":{"$ref":"#/components/responses/unprocessable_entity"}},"summary":"Update itinerary line","tags":["posts"]},"put":{"callbacks":{},"description":"Updates an existing itinerary line.","operationId":"TatexApiWeb.PostItineraryController.update","parameters":[{"$ref":"#/components/parameters/x_agency"},{"description":"Post ID.","in":"path","name":"post_id","required":true,"schema":{"minimum":1,"type":"integer"}},{"description":"Itinerary line ID.","in":"path","name":"id","required":true,"schema":{"minimum":1,"type":"integer"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItineraryRequest"}}},"description":"Itinerary line attributes.","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItineraryResponse"}}},"description":"Updated itinerary line."},"400":{"$ref":"#/components/responses/bad_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"},"422":{"$ref":"#/components/responses/unprocessable_entity"}},"summary":"Update itinerary line","tags":["posts"]}},"/api/posts/{post_id}/itinerary/{post_itinerary_id}/attachments":{"post":{"callbacks":{},"description":"Uploads one or more files and links them to an itinerary line.\n\n**Request:** `multipart/form-data` with at least one `uploads` field.\n","operationId":"TatexApiWeb.PostItineraryAttachmentController.create","parameters":[{"$ref":"#/components/parameters/x_agency"},{"description":"Post ID.","in":"path","name":"post_id","required":true,"schema":{"minimum":1,"type":"integer"}},{"description":"Itinerary line ID.","in":"path","name":"post_itinerary_id","required":true,"schema":{"minimum":1,"type":"integer"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/ItineraryAttachmentUploadRequest"}}},"description":"File uploads.","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItineraryResponse"}}},"description":"Itinerary line with new attachments."},"400":{"$ref":"#/components/responses/bad_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"}},"summary":"Upload itinerary attachments","tags":["posts"]}},"/api/posts/{post_id}/itinerary/{post_itinerary_id}/attachments/{id}":{"delete":{"callbacks":{},"description":"Removes an attachment from an itinerary line.","operationId":"TatexApiWeb.PostItineraryAttachmentController.delete","parameters":[{"$ref":"#/components/parameters/x_agency"},{"description":"Post ID.","in":"path","name":"post_id","required":true,"schema":{"minimum":1,"type":"integer"}},{"description":"Itinerary line ID.","in":"path","name":"post_itinerary_id","required":true,"schema":{"minimum":1,"type":"integer"}},{"description":"Attachment ID.","in":"path","name":"id","required":true,"schema":{"minimum":1,"type":"integer"}}],"responses":{"204":{"$ref":"#/components/responses/no_content"},"400":{"$ref":"#/components/responses/bad_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"}},"summary":"Delete itinerary attachment","tags":["posts"]}}},"security":[{"bearerAuth":[]}],"servers":[{"url":"https://api-edge.gttwl.net","variables":{}}],"tags":[{"description":"Discover agencies the authenticated user may publish to. Returns `cname` values\nfor the `X-Agency` header. Does **not** require `X-Agency`.\n","name":"agencies"},{"description":"List publish-as targets (`zid`, name, email, role) for the `X-Agency` agency.\nAdmin-grade callers only. Requires `agencies:read` and `X-Agency`.\n","name":"advisors"},{"description":"Global editorial taxonomy. Returns `id`, `title`, and `permalink`. Use\n`permalink` when creating or updating posts. Does **not** require `X-Agency`.\n","name":"categories"},{"description":"Create, read, update, and delete posts (blog, products, itineraries, etc.).\nRequires `X-Agency`. Nested routes cover attachments and itinerary lines.\nSupports SEO `data`, ordered `faqs`, provenance (`src`/`src_id`),\ndeterministic hero designation via `primary=true` on attachment upload, and\nadmin-grade publish-as via `author.zid`.\n","name":"posts"}]}