{"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":{"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"},"ProfileRequest":{"description":"Sparse profile write. PUT and PATCH behave identically.","example":{"profile":{"about":"<p>Third-person bio…</p>","name":"Madeline Advisor","social_links":{"Social.com":"https://social.com","facebook":"https://facebook.com/madeline"}}},"properties":{"profile":{"anyOf":[{"$ref":"#/components/schemas/Profile"}],"description":"Profile attributes."}},"required":["profile"],"title":"ProfileRequest","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"},"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"},"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"},"Profile":{"additionalProperties":true,"description":"Syndicated advisor profile. Wire socials are a single `social_links` map\n(any platform key). Known hosts (`facebook`, `instagram`, `twitter`,\n`linkedin`, `youtube`, `google`, `tiktok`, `pinterest`, `google_maps`) are\nstored as bare URLs; the six CMS defaults also land on `social_*` columns.\nCustom platforms are stored as markdown `[Label](https://url)`.\n`specialties` and `credentials` are comma-separated strings from `users.data`.\n`thumbnail_url` is an attachment id string, not a CDN URL.\n","example":{"name":"Madeline Advisor","social_links":{"facebook":"https://facebook.com/madeline","social_com":"[Social.com](https://social.com)"},"title":"Travel Advisor","username":"madeline","zid":"user-zid-uuid"},"properties":{"about":{"description":"Bio HTML, max 10 000 characters.","nullable":true,"type":"string"},"author":{"allOf":[{"$ref":"#/components/schemas/AuthorOverride"}],"description":"Admin-grade target on write. Omit for the token user."},"credentials":{"example":"CTA, Virtuoso","nullable":true,"oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"},{"type":"null"}]},"name":{"example":"Madeline Advisor","nullable":true,"type":"string"},"organization":{"example":"Largay Travel","nullable":true,"type":"string"},"social_links":{"additionalProperties":{"nullable":true,"type":"string"},"description":"Source of truth for social URLs. Open map: extra platforms are kept.\nKnown hosts → bare `https://…` URL. Custom hosts → `[Label](https://url)`.\nSparse on write — absent key unchanged, `null`/`\"\"` clears that host.\n","example":{"facebook":"https://facebook.com/madeline","social_com":"[Social.com](https://social.com)","tiktok":"https://tiktok.com/@madeline"},"nullable":true,"type":"object"},"specialties":{"description":"On write, array or string. Stored and returned as a comma-separated string.","example":"Japan, Luxury rail","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"},{"type":"null"}]},"src":{"example":"amplify","nullable":true,"type":"string"},"src_id":{"example":"advisor-123","nullable":true,"type":"string"},"thumbnail_url":{"description":"Attachment id as a string.","example":"88421","nullable":true,"readOnly":true,"type":"string"},"title":{"example":"Travel Advisor","nullable":true,"type":"string"},"updated_at":{"format":"date-time","readOnly":true,"type":"string"},"url":{"example":"https://example.com","nullable":true,"type":"string"},"username":{"example":"madeline","readOnly":true,"type":"string"},"zid":{"description":"TAT user zid.","example":"user-zid-uuid","readOnly":true,"type":"string"}},"title":"Profile","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,"pubdate":"2024-10-15T12:00:00Z","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). `supplier_tags` is reserved\n(written by the API when `tags` is present) and is ignored if sent.\n","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"},"expires":{"description":"When set, the post is treated as inactive after this datetime. Omit to leave\nunchanged.\n","example":"2026-12-31T23:59:59Z","format":"date-time","nullable":true,"type":"string"},"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`, up to 20 characters).","example":"en","maxLength":20,"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"},"pubdate":{"description":"Public \"published as of\" datetime (ISO 8601). Does not hide the post by itself;\nvisibility is `active` and optional `expires`. Omit to leave unchanged.\n","example":"2024-10-15T12:00:00Z","format":"date-time","nullable":true,"type":"string"},"src":{"description":"Provenance source system identifier (your product or integration name).","example":"partner_app","maxLength":255,"type":"string"},"src_id":{"description":"External record id in the source system (for sync and support).","example":"ext_story_abc123","maxLength":255,"type":"string"},"summary":{"description":"Short teaser for listing views.","example":"An 8-day journey from Mexico City into the golden marigold fields of Atlixco.","type":"string"},"tags":{"description":"Comma-separated tags from the connector (stored lowercase). When present, tags\nthat are not already on the post are stored on reserved `data.supplier_tags` and\nunioned onto `posts.tags` (TAT-owned tags stay). `null` or `\"\"` clears\n`data.supplier_tags` and removes those tags from `posts.tags`. Omit the key to\nleave tags unchanged. Do not set `data.supplier_tags` yourself.\n","example":"gadventure,hiking,rainforest","nullable":true,"type":"string"},"title":{"description":"Headline (up to 1024 characters).","example":"Mexico City to Oaxaca: Celebrate Day of the Dead","maxLength":1024,"minLength":1,"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"},"ItineraryResponse":{"description":"Single itinerary line response.","properties":{"data":{"allOf":[{"$ref":"#/components/schemas/Itinerary"}],"description":"Itinerary line resource."}},"required":["data"],"title":"ItineraryResponse","type":"object"},"Advisor":{"additionalProperties":false,"description":"Agency member eligible as a publish-as target (role > 1).","example":{"email":"madeline@example.com","name":"Madeline Advisor","posts_count":12,"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"},"posts_count":{"description":"Number of posts authored by this user (`posts.user_id`).","example":12,"minimum":0,"type":"integer"},"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","posts_count"],"title":"Advisor","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"},"ProfileResponse":{"description":"Profile envelope. Same `data` object on GET and PUT.","properties":{"data":{"allOf":[{"$ref":"#/components/schemas/Profile"}],"description":"Stored profile."}},"required":["data"],"title":"ProfileResponse","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"},"AdvisorsResponse":{"description":"Paginated list of publish-as targets for the X-Agency agency.","example":{"data":[{"email":"madeline@example.com","name":"Madeline Advisor","posts_count":12,"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"},"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"},"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"},"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"},"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"},"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"},"ProfilePhotoResponse":{"description":"Uploaded profile photo.","properties":{"data":{"properties":{"attachment_id":{"example":88421,"format":"int64","type":"integer"},"url":{"example":"https://cdn1.gttwl.net/attachments/…","format":"uri","type":"string"}},"required":["attachment_id","url"],"type":"object"}},"required":["data"],"title":"ProfilePhotoResponse","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"},"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"},"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"},"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"},"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"},"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"},"ProfilePhotoUploadRequest":{"description":"Multipart headshot. Field name is `uploads` (one file).","properties":{"uploads":{"description":"Profile photo file.","format":"binary","type":"string"}},"required":["uploads"],"title":"ProfilePhotoUploadRequest","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"},"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"},"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"}},"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`, `profile: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| `profile:write` | `GET`/`PUT`/`PATCH /api/profile`, `POST /api/profile/photo` |\n\nEmpty-scope tokens are rejected for API calls. Connector clients should request:\n`openid profile agencies:read categories:read posts:read posts:write profile:write`.\nIf `categories:read` is on the client allow-list but missing from an existing grant,\nthe next refresh adds it (no reconnect, no token revoke).\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}`. Optional `tags` are\n   backed up on `data.supplier_tags` and synced onto `posts.tags` without removing\n   TAT-owned tags (`null`/`\"\"` resets supplier tags only). Optional `pubdate` /\n   `expires` set the public published-at and expiry datetimes. 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. When `tags` is present it syncs supplier tags (backed\n   up on `data.supplier_tags`); `null`/`\"\"` resets supplier tags only.\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## Profile syndication\n\nRequires `profile:write` (OIDC `profile` is not sufficient). `PUT` and `PATCH`\n`/api/profile` are identical sparse writes. Omit `zid` on `GET` / `POST …/photo`\nfor the token user; admin-grade callers may target another member (`?zid=` or\n`author.zid`). Wire socials as one `social_links` map (known hosts = bare URL;\ncustom = `[Label](https://url)`); the six CMS defaults also land on `social_*`\ncolumns. `specialties` / `credentials` merge into `users.data`.\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` and `POST /api/profile/photo` |\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**Tags:** when `tags` is present, the payload is stored on reserved\n`data.supplier_tags` and synced onto `posts.tags` without removing TAT-owned\ntags. `null` or `\"\"` clears supplier tags (TAT tags stay). Omit the key to\nleave both unchanged. Do not set `data.supplier_tags` yourself — it is ignored.\n\n**Dates:** `pubdate` is the public \"published as of\" datetime. It does not hide the\npost by itself — visibility is `active` and optional `expires`. `expires` hides the\npost after that datetime.\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.\nOmitted keys are left unchanged. When `faqs` is present it **fully replaces** the\nFAQ set (no append). When `tags` is present it syncs supplier tags onto the post\n(see create); `null`/`\"\"` resets supplier tags only.\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.\nOmitted keys are left unchanged. When `faqs` is present it **fully replaces** the\nFAQ set (no append). When `tags` is present it syncs supplier tags onto the post\n(see create); `null`/`\"\"` resets supplier tags only.\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\nMalformed attachment IDs return `400`. Unknown IDs, or IDs that do not belong to this post, return `404`.\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.\n\nMalformed attachment IDs return `400`. Unknown IDs, or IDs that do not belong to this itinerary line, return `404`.\n","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"]}},"/api/profile":{"get":{"callbacks":{},"description":"Current stored profile for the token user, or another agency member when\n`zid` is supplied (admin-grade 4-check). Same `data` object as PUT.\n\nRequires `profile:write` (no separate `profile:read` scope). Empty stored\nvalues are `null`. Does not return CMS-owned fields (email, phone, password,\nnotes, address). `social_links` is the only socials wire shape.\n","operationId":"TatexApiWeb.ProfileController.show","parameters":[{"$ref":"#/components/parameters/x_agency"},{"description":"Target member zid. Omit for the token user. Admin-grade callers only when set.","in":"query","name":"zid","required":false,"schema":{"example":"advisor-zid-uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"}}},"description":"Stored profile."},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"}},"summary":"Get profile","tags":["profile"]},"patch":{"callbacks":{},"description":"Sparse profile write. `PUT` and `PATCH` are identical: present keys are\nwritten, absent keys are left unchanged, `null` or `\"\"` clears a field.\n\n`social_links` is the source of truth (open map, any platform key). Known\nhosts stay bare URLs; custom hosts are stored as `[Label](https://url)`.\nAfter a map write, the six CMS defaults (`facebook`, `instagram`, `twitter`,\n`linkedin`, `youtube`, `google`) are copied onto `social_*` columns.\n\n`specialties` / `credentials` merge into `users.data` as comma-separated\nstrings and never replace other `data` keys. `faqs` is ignored in v1.\n\nOmit `author` to write the token user. Admin-grade callers may send\n`author: {\"zid\"}` (same 4-check as posts). Unknown zid → 404. No create.\n","operationId":"TatexApiWeb.ProfileController.update (2)","parameters":[{"$ref":"#/components/parameters/x_agency"}],"requestBody":{"content":{"application/json":{"example":{"profile":{"about":"<p>Third-person bio…</p>","credentials":["CTA"],"name":"Madeline Advisor","organization":"Largay Travel","social_links":{"Social.com":"https://social.com","facebook":"https://facebook.com/madeline","tiktok":"https://tiktok.com/@madeline"},"specialties":["Japan","Luxury rail"],"src":"amplify","src_id":"advisor-123","title":"Travel Advisor","url":"https://example.com"}},"schema":{"$ref":"#/components/schemas/ProfileRequest"}}},"description":"Profile attributes.","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"}}},"description":"Updated profile."},"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 profile","tags":["profile"]},"put":{"callbacks":{},"description":"Sparse profile write. `PUT` and `PATCH` are identical: present keys are\nwritten, absent keys are left unchanged, `null` or `\"\"` clears a field.\n\n`social_links` is the source of truth (open map, any platform key). Known\nhosts stay bare URLs; custom hosts are stored as `[Label](https://url)`.\nAfter a map write, the six CMS defaults (`facebook`, `instagram`, `twitter`,\n`linkedin`, `youtube`, `google`) are copied onto `social_*` columns.\n\n`specialties` / `credentials` merge into `users.data` as comma-separated\nstrings and never replace other `data` keys. `faqs` is ignored in v1.\n\nOmit `author` to write the token user. Admin-grade callers may send\n`author: {\"zid\"}` (same 4-check as posts). Unknown zid → 404. No create.\n","operationId":"TatexApiWeb.ProfileController.update","parameters":[{"$ref":"#/components/parameters/x_agency"}],"requestBody":{"content":{"application/json":{"example":{"profile":{"about":"<p>Third-person bio…</p>","credentials":["CTA"],"name":"Madeline Advisor","organization":"Largay Travel","social_links":{"Social.com":"https://social.com","facebook":"https://facebook.com/madeline","tiktok":"https://tiktok.com/@madeline"},"specialties":["Japan","Luxury rail"],"src":"amplify","src_id":"advisor-123","title":"Travel Advisor","url":"https://example.com"}},"schema":{"$ref":"#/components/schemas/ProfileRequest"}}},"description":"Profile attributes.","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"}}},"description":"Updated profile."},"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 profile","tags":["profile"]}},"/api/profile/photo":{"post":{"callbacks":{},"description":"Multipart upload of one headshot. Field name is `uploads` (not `file`).\nCreates an `attachments` row (`kind: \"User\"`) and sets `users.thumbnail_url`\nto that attachment id as a string.\n\nPreferred size: square, longest side ≤ 1200 (guidance; other sizes are accepted).\nOptional `zid` query param uses the same 4-check as GET.\n","operationId":"TatexApiWeb.ProfileController.photo","parameters":[{"$ref":"#/components/parameters/x_agency"},{"description":"Target member zid. Omit for the token user. Admin-grade callers only when set.","in":"query","name":"zid","required":false,"schema":{"example":"advisor-zid-uuid","type":"string"}}],"requestBody":{"content":{"multipart/form-data":{"example":{"uploads":"<binary file>"},"schema":{"$ref":"#/components/schemas/ProfilePhotoUploadRequest"}}},"description":"Profile photo.","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfilePhotoResponse"}}},"description":"Uploaded photo."},"400":{"$ref":"#/components/responses/bad_request"},"401":{"$ref":"#/components/responses/unauthorized"},"403":{"$ref":"#/components/responses/forbidden"},"404":{"$ref":"#/components/responses/not_found"}},"summary":"Upload profile photo","tags":["profile"]}}},"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"},{"description":"Read and sparse-update the token user's TAT member profile, or another\nagency member via `zid` / `author.zid` (admin-grade 4-check). Requires\n`profile:write` and `X-Agency`. `social_links` is the only socials wire\nshape (known hosts = bare URL; custom = `[Label](https://url)`).\nHeadshot upload is `POST /api/profile/photo` (`uploads`).\n","name":"profile"}]}