{"openapi":"3.1.0","info":{"title":"Tuti API","version":"1","description":"Read and change a project's feedback. Authenticate with a project API key (Settings → Developers): `Authorization: Bearer tuti_…`. Up to 600 requests a minute per key."},"servers":[{"url":"https://tuti.so/api/v1"}],"security":[{"apiKey":[]}],"paths":{"/project":{"get":{"operationId":"project.get","summary":"Get the project the key belongs to, with its portal URL and what the team said it will use (`uses`, project.update's)","tags":["project"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"patch":{"operationId":"project.update","summary":"Rename the project (the name its portal, emails and the app show; its address stays the same), pick Who's asking: the number property of your users (users.sync's `properties`, like `mrr`) the app totals over each post's voters for the team (null: none), pick `userColumns`: the properties Users shows as columns, in order (up to 10), or set `propertyFormats`: how the app shows each property, by name (a `label` of the team's own, and a `format`: text, number, money in a `currency`, or date; all of them at once, up to 100), or say what the team will `uses` (feedback, docs, changelog, support: Home's Get set up follows it; none: every step)","tags":["project"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":60},"askingProperty":{"anyOf":[{"type":"string","minLength":1,"maxLength":64},{"type":"null"}]},"userColumns":{"maxItems":10,"type":"array","items":{"type":"string","minLength":1,"maxLength":64}},"propertyFormats":{"type":"object","propertyNames":{"type":"string","minLength":1,"maxLength":64},"additionalProperties":{"type":"object","properties":{"label":{"type":"string","maxLength":64},"format":{"type":"string","enum":["text","number","money","date"]},"currency":{"type":"string","enum":["USD","EUR","GBP","CAD","AUD","CHF","SEK","INR","JPY","BRL"]}}}},"uses":{"type":"array","items":{"type":"string","enum":["feedback","docs","changelog","support"]}}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/project/property-formats":{"patch":{"operationId":"propertyFormats.update","summary":"Set how the app shows some of your users' properties, by name, keeping the rest (project.update's `propertyFormats` sets them all at once): a `label` of the team's own and a `format` (text, number, money in a `currency`, or date); one with neither is cleared","tags":["propertyFormats"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"formats":{"type":"object","propertyNames":{"type":"string","minLength":1,"maxLength":64},"additionalProperties":{"type":"object","properties":{"label":{"type":"string","maxLength":64},"format":{"type":"string","enum":["text","number","money","date"]},"currency":{"type":"string","enum":["USD","EUR","GBP","CAD","AUD","CHF","SEK","INR","JPY","BRL"]}}}}},"required":["formats"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/team":{"get":{"operationId":"team.list","summary":"List the project's team: names and roles","tags":["team"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/conversations":{"get":{"operationId":"conversations.list","summary":"List conversations, latest message first; spam is left out unless asked for. `snoozed` true lists only those snoozed now, false leaves them out","tags":["conversations"],"parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["open","waiting","closed"]}},{"name":"assignee","in":"query","required":false,"schema":{"anyOf":[{"type":"string","const":"none"},{"type":"string","format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}]}},{"name":"user","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":255}},{"name":"spam","in":"query","required":false,"schema":{"type":"string"}},{"name":"snoozed","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"default":25,"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"schema":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"conversations.create","summary":"Start a conversation from someone in your app (`user`, by their id there) or an email address, with their first message","tags":["conversations"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":255},"name":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"required":["id"]},"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"subject":{"type":"string","minLength":1,"maxLength":200},"body":{"type":"string","minLength":1,"maxLength":10000}},"required":["body"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/conversations/{number}":{"get":{"operationId":"conversations.get","summary":"Get a conversation by its number, with its messages, oldest first","tags":["conversations"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"patch":{"operationId":"conversations.update","summary":"Change a conversation's status (waiting on the user only once the team's reply is its latest message: replying sets it), assign it to a team member (by email; null to unassign), mark it as spam, or snooze it till a time to come (`snoozedUntil`, up to a year; null wakes it): out of the inbox's views till then, or till the user writes, and open again after","tags":["conversations"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["open","waiting","closed"]},"assignee":{"anyOf":[{"type":"string","format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]},"spam":{"type":"boolean"},"snoozedUntil":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},{"type":"null"}]}},"required":[],"anyOf":[{"required":["status"]},{"required":["assignee"]},{"required":["spam"]},{"required":["snoozedUntil"]}]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/conversations/{number}/messages":{"post":{"operationId":"conversations.reply","summary":"Reply in a conversation as the team (and close it with `close`: `closed` says whether it did; `keptOpen`, that it stays open and waiting on the team as the user wrote after what the team saw, `seen`, unanswered), write an internal note (`note`: only the team sees it, never sent), or write as its user (`author`, by their id in your app)","tags":["conversations"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":10000},"author":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":255}},"required":["id"]},"note":{"description":"An internal note: only the team sees it; it isn't sent and doesn't change the status","type":"boolean"},"close":{"description":"The team's reply closes the conversation (Send and close), rather than leaving it waiting on the user","type":"boolean"},"seen":{"description":"When the latest message the team saw was written: one from the user after it keeps the conversation open and waiting on the team (without it, the latest as the reply is asked for)","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},"attachments":{"description":"Files uploaded for it in the app (their ids), sent with it: 10 MB each, 25 MB together","maxItems":10,"type":"array","items":{"type":"string","minLength":1}}},"required":["body"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/conversations/{number}/posts":{"post":{"operationId":"conversations.linkPost","summary":"Link a conversation to a post it's about; merging the post takes the link along","tags":["conversations"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"post":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["post"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/conversations/{number}/posts/{post}":{"delete":{"operationId":"conversations.unlinkPost","summary":"Unlink a conversation from a post","tags":["conversations"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},{"name":"post","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/conversations/{number}/suggested-reply":{"post":{"operationId":"conversations.suggestReply","summary":"A reply drafted from your docs to what the user asked, for the team to edit and send (nothing is sent). Only when the docs cover it: otherwise `no_answer`, without asking the AI","tags":["conversations"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/conversations/{number}/summary":{"post":{"operationId":"conversations.summarize","summary":"Adds the AI's summary of the conversation as a note (only the team sees it): what the user wants, what's been done and what's open","tags":["conversations"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/support":{"get":{"operationId":"support.get","summary":"Get the support inbox's settings: whether it's on (Messages in the widget; \"Ask a person\" starts a conversation), and its email address (null until it has one; `email` is null where the server doesn't receive email)","tags":["support"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"patch":{"operationId":"support.update","summary":"Turn the support inbox on or off (it takes “Ask a person” after an AI answer too, docs.escalate), or (`email: true`) give the project its support email address, which is its for good","tags":["support"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"inbox":{"type":"boolean"},"email":{"type":"boolean","const":true}},"required":[],"anyOf":[{"required":["inbox"]},{"required":["email"]}]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/support/email-domain":{"post":{"operationId":"support.setEmailDomain","summary":"Send replies by email from your own address (`support@yourcompany.com`): adds its domain to Amazon SES, whose 3 DKIM CNAMEs (`support.get`, `email.ownDomain.records`) go in your DNS. Replies come from it once SES finds them (`support.checkEmailDomain`); until then, from the project's support address. A domain is one project's alone. Pro plan","tags":["support"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"address":{"type":"string","maxLength":254,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"required":["address"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"delete":{"operationId":"support.removeEmailDomain","summary":"Stop sending from your own domain: replies come from the project's support address again, and Amazon SES forgets the domain","tags":["support"],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/support/email-domain/check":{"post":{"operationId":"support.checkEmailDomain","summary":"Ask Amazon SES now whether your domain's DKIM records are in place (it checks again daily on its own)","tags":["support"],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/boards":{"get":{"operationId":"boards.list","summary":"List the project's boards","tags":["boards"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/roadmap":{"get":{"operationId":"roadmap.get","summary":"The public roadmap: planned, in progress and completed posts","tags":["roadmap"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/changelog":{"get":{"operationId":"changelog.list","summary":"List changelog entries, newest first: published ones, or the team's drafts","tags":["changelog"],"parameters":[{"name":"status","in":"query","required":false,"schema":{"default":"published","type":"string","enum":["published","draft"]}},{"name":"limit","in":"query","required":false,"schema":{"default":20,"type":"integer","minimum":1,"maximum":50}},{"name":"cursor","in":"query","required":false,"schema":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"changelog.create","summary":"Write a changelog entry (a draft until it's published), pick the posts it completes and its cover image (an upload's id)","tags":["changelog"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":3,"maxLength":120},"body":{"type":"string","maxLength":20000},"content":{},"tags":{"default":[],"maxItems":4,"type":"array","items":{"type":"string","enum":["new","improved","fixed","breaking"]}},"posts":{"default":[],"maxItems":50,"type":"array","items":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"cover":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]}},"required":["title"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/changelog/{number}":{"get":{"operationId":"changelog.get","summary":"Get a changelog entry by its number (drafts only for the team)","tags":["changelog"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"patch":{"operationId":"changelog.update","summary":"Change a changelog entry: its title, text, tags, cover image (an upload's id, null to remove) or the posts it completes (posts replace the list; only while it's a draft)","tags":["changelog"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":3,"maxLength":120},"body":{"type":"string","maxLength":20000},"content":{},"tags":{"maxItems":4,"type":"array","items":{"type":"string","enum":["new","improved","fixed","breaking"]}},"posts":{"maxItems":50,"type":"array","items":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"cover":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"expectedUpdatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"delete":{"operationId":"changelog.delete","summary":"Delete a changelog entry","tags":["changelog"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/changelog/{number}/reactions":{"post":{"operationId":"changelog.react","summary":"React to a published changelog entry (thumbs_up 👍, heart ❤️ or tada 🎉) or take the reaction back, as the key's creator or on behalf of someone from your app","tags":["changelog"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reaction":{"type":"string","enum":["thumbs_up","heart","tada"]},"reacted":{"type":"boolean"},"author":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":255},"name":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"required":["id"]}},"required":["reaction","reacted"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/changelog/seen":{"post":{"operationId":"changelog.seen","summary":"Mark the announced changelog entries published up to `at` (default: now) as seen, as the key's creator or on behalf of someone from your app; it never moves back","tags":["changelog"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"},"author":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":255},"name":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"required":["id"]}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/changelog/{number}/draft":{"post":{"operationId":"changelog.draft","summary":"Replace a draft entry's text with release notes the AI writes from its pull requests and the posts it completes (and name it, if it still has the running draft's title)","tags":["changelog"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/changelog/{number}/rewrite":{"post":{"operationId":"changelog.rewrite","summary":"Propose an AI edit of an entry's text, without saving it: `shorten` it, explain it for non-developers (`plain`), or rewrite a `section`; the whole text, or one section (`section`: its heading, or its number: 1 for the first heading, 0 for the text before it), with an optional `instruction`. To accept it, send `proposed.content` to changelog.update with `expectedUpdatedAt`","tags":["changelog"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["shorten","plain","section"]},"section":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"string","minLength":1,"maxLength":200}]},"instruction":{"type":"string","maxLength":200}},"required":["mode"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/changelog/{number}/publish":{"post":{"operationId":"changelog.publish","summary":"Publish a changelog entry on the portal; the posts it completes become Completed. Announced (the default) it shows as new in the widget; `announce: false` publishes it quietly. With `emailSubscribers` it's emailed to the changelog's confirmed subscribers (a public portal's), the first time it's published that way only. Sample entries (Home's sample content) are always published quietly, and never emailed","tags":["changelog"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"announce":{"default":true,"type":"boolean"},"emailSubscribers":{"default":false,"type":"boolean"}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/changelog/{number}/unpublish":{"post":{"operationId":"changelog.unpublish","summary":"Take a changelog entry off the portal (back to a draft); posts keep their status","tags":["changelog"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/changelog-style":{"get":{"operationId":"changelogStyle.get","summary":"Get the changelog's writing style: the tone notes and example entries AI drafts follow","tags":["changelogStyle"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"patch":{"operationId":"changelogStyle.update","summary":"Set the changelog's writing style for AI drafts: tone notes, and up to 3 published entries (by number) as examples (replaces the list)","tags":["changelogStyle"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tone":{"type":"string","maxLength":1000},"examples":{"maxItems":3,"type":"array","items":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/articles":{"get":{"operationId":"articles.list","summary":"List docs articles by title: published ones (listed everywhere; the team sees every listing), the team's drafts (latest edited first), or deleted ones the team can still restore (latest deleted first)","tags":["articles"],"parameters":[{"name":"status","in":"query","required":false,"schema":{"default":"published","type":"string","enum":["published","draft","deleted"]}},{"name":"limit","in":"query","required":false,"schema":{"default":50,"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"schema":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"articles.create","summary":"Write a docs article (a draft until it's published); its slug comes from the title, unless you set one (`slug`: lowercase letters, digits and hyphens; refused if it's taken, never changed). `access` says who can read it once it's published, on top of its collection and tab (`inherit`, the default; `signed_in`; or `groups`, with `groups`), and `noAccess` what people it isn't for see of it (`inherit`, the default: as its collection; `lock`: its title with a lock, never its text; or `hide`). `description`, `keywords` and `noindex` as in articles.update","tags":["articles"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":3,"maxLength":120},"body":{"type":"string","maxLength":50000},"content":{},"collection":{"anyOf":[{"type":"string","minLength":1,"maxLength":80},{"type":"null"}]},"showUpdatedDate":{"type":"boolean"},"listing":{"type":"string","enum":["everywhere","unlisted","hidden","ai_only"]},"description":{"type":"string","maxLength":300},"keywords":{"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":60}},"noindex":{"type":"boolean"},"access":{"type":"string","enum":["inherit","signed_in","groups"]},"groups":{"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":50,"pattern":"^[a-z0-9][a-z0-9_.:-]*$"}},"noAccess":{"type":"string","enum":["inherit","lock","hide"]},"slug":{"type":"string","minLength":1,"maxLength":80,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$"}},"required":["title"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/articles/{slug}":{"get":{"operationId":"articles.get","summary":"Get a docs article by its slug (drafts, hidden and AI-only articles only for the team). The team also gets its unpublished `changes`","tags":["articles"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"patch":{"operationId":"articles.update","summary":"Change a docs article: its title, text, collection or `listing` (everywhere, unlisted, hidden or ai_only) (a draft's slug follows its title; a published one keeps it). Each change is kept as a version (see articles.revisions). On a published article, changes to the title, text and collection are kept as unpublished `changes` (readers see the published version until articles.publishChanges); with `publish: true` they go live at once, with any changes saved before (refused, 402 `article_limit`, while the project has more published articles than its plan allows; saving alone always works). The listing, who can read it and what the rest see (`access`, `groups` and `noAccess`, as in articles.create), showUpdatedDate and its settings change at once: `newSlug` sets the end of its URL (as `slug` in articles.create; on a published one too, and the old address redirects), null goes back to one from the title (a draft's follows it again; a published one keeps the one it has); `description` (the meta description; empty: from the text), `keywords` (searched like the text) and `noindex` (block search indexing)","tags":["articles"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":3,"maxLength":120},"body":{"type":"string","maxLength":50000},"content":{},"collection":{"anyOf":[{"type":"string","minLength":1,"maxLength":80},{"type":"null"}]},"showUpdatedDate":{"type":"boolean"},"listing":{"type":"string","enum":["everywhere","unlisted","hidden","ai_only"]},"description":{"type":"string","maxLength":300},"keywords":{"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":60}},"noindex":{"type":"boolean"},"access":{"type":"string","enum":["inherit","signed_in","groups"]},"groups":{"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":50,"pattern":"^[a-z0-9][a-z0-9_.:-]*$"}},"noAccess":{"type":"string","enum":["inherit","lock","hide"]},"newSlug":{"anyOf":[{"type":"string","minLength":1,"maxLength":80,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$"},{"type":"null"}]},"publish":{"type":"boolean"},"revision":{"type":"string","minLength":1},"seen":{"type":"string","minLength":1},"own":{"type":"string","minLength":1},"edited":{"maxItems":1000,"type":"array","items":{"type":"string","maxLength":2000}}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"delete":{"operationId":"articles.delete","summary":"Delete a docs article; the team can restore it (with articles.restore) for 30 days","tags":["articles"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/search/articles":{"get":{"operationId":"articles.search","summary":"Search the published docs articles: best matches first, each with the passage that matched. Each word also matches the words it starts (\"sig\" finds \"signing\"), so it works as someone types","tags":["articles"],"parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":200}},{"name":"limit","in":"query","required":false,"schema":{"default":10,"type":"integer","minimum":1,"maximum":20}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/articles/{slug}/feedback":{"post":{"operationId":"articles.feedback","summary":"Answer “Was this helpful?” under a published help article (a “No” may say what was missing), as the key's creator or on behalf of someone from your app; answering again replaces the answer","tags":["articles"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"helpful":{"type":"boolean"},"comment":{"type":"string","maxLength":500},"author":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":255},"name":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"required":["id"]}},"required":["helpful"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/docs/ask":{"post":{"operationId":"docs.ask","summary":"Ask the docs a question: an AI answer from the published articles with the articles it cites, `no_answer` (“I don't know”) when they don't cover it, `off_limits` when it names one of the team's off-limits topics (see assistant.update; the AI isn't asked), or `unavailable` with the closest articles when the project is out of AI credits (the AI isn't asked). To ask a follow-up, pass `followUpTo`: the `id` of the answer it follows (one this key asked); the last 4 turns are its context, for finding what to answer from and for the AI, which still answers only from the articles. Off (503) unless the server has AI and embeddings set up; up to 20 questions an hour per key","tags":["docs"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"q":{"type":"string","minLength":1,"maxLength":300},"followUpTo":{"type":"string","minLength":1,"maxLength":100}},"required":["q"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/docs/escalate":{"post":{"operationId":"docs.escalate","summary":"Pass a question to the team (“Ask a person”), for someone docs.ask couldn't help: it starts a conversation in the support inbox with the question and the note (plain text, up to 1,000 characters) from `email` (a key's) or the user acting, the AI's answer with the articles it cited, and a note for the team with a short AI summary. Only for a question this caller asked in the last hour, once; refused (403) while the support inbox is off or Messages is hidden in the widget, and for a key (409) while replies can't be emailed (the server doesn't send support email, the project has no support address, or the address marked its email as spam)","tags":["docs"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"questionId":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","maxLength":320},"note":{"type":"string","maxLength":1000}},"required":["questionId","email"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/assistant":{"get":{"operationId":"assistant.get","summary":"Get how the docs' AI answers: tone, answer length, standing instructions and off-limits topics","tags":["assistant"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"patch":{"operationId":"assistant.update","summary":"Set how the docs' AI answers: tone, answer length (short, medium or detailed), standing instructions, and up to 20 off-limits topics (replaces the list). A question that names an off-limits topic gets “I can't help with that here” without asking the AI. None of it overrides answering only from the docs","tags":["assistant"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tone":{"type":"string","maxLength":200},"length":{"type":"string","enum":["short","medium","detailed"]},"instructions":{"type":"string","maxLength":2000},"offLimits":{"maxItems":20,"type":"array","items":{"type":"string","maxLength":60}}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/docs/insights":{"get":{"operationId":"docs.insights","summary":"How the docs did in the last 30 days: views, the top articles with how many readers found them helpful, the latest “No” comments, the searches that found nothing, and the gaps: questions the AI couldn't answer (with why, the latest time) or whose answer got a thumbs-down, grouped (case and spacing don't matter), with how many were passed to the team (docs.escalate). Where plans are sold, on Pro and Business, as Reporting (402 otherwise)","tags":["docs"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/docs/gaps/dismiss":{"post":{"operationId":"docs.dismissGap","summary":"Dismiss a question from the gaps and the searches that found nothing in docs.insights (matched like the list groups them: case and spacing don't matter); asked again later, it's back (a search from the next day on, UTC: searches are counted per day)","tags":["docs"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"question":{"type":"string","minLength":1,"maxLength":300}},"required":["question"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/reports":{"get":{"operationId":"reports.get","summary":"How a range of days went (`range`: today, yesterday, month, last-month, 7d, 14d, 30d, quarter, year; the past 30 days by default), by day (UTC), in total and in total over as many days just before (`before`, to compare with; while today is under way, the last of them only as much of it as today has had, so possibly fractions; null where those were deleted, as Past year's are on servers with plans, and searches null where theirs were): the docs (views, searches and those that found nothing, readers' answers to “Was this helpful?”, each article's numbers), the AI's conversations from the docs (answered, passed to the team, thumbs up and down, the latest ones, the questions it couldn't answer) and the inbox (new conversations by where they began, the median time to the team's first reply, open now, replies per teammate). Where plans are sold, on Pro, ranges up to a month, and Business, every range (402 otherwise)","tags":["reports"],"parameters":[{"name":"range","in":"query","required":false,"schema":{"default":"30d","type":"string","enum":["today","yesterday","month","last-month","7d","14d","30d","quarter","year"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/articles/{slug}/publish":{"post":{"operationId":"articles.publish","summary":"Publish a docs article on the portal. Refused (402 `article_limit`) where the project's plan has a limit on published articles and it would go past it. While Autopilot's edits wait in the draft (they go live with it), `edits` is required (409 `edits_required` without it): the ids of those you reviewed (codeChanges.target's `actions`); one waiting that isn't among them is a 409 `changed` (one gone since is fine)","tags":["articles"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"edits":{"maxItems":500,"type":"array","items":{"type":"string","minLength":1,"maxLength":100}}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/articles/{slug}/publish-changes":{"post":{"operationId":"articles.publishChanges","summary":"Publish a docs article's unpublished `changes` (see articles.update): readers, search and AI answers get them, and Autopilot's edits waiting in them are approved. Without changes it does nothing. Refused (402 `article_limit`) while the project has more published articles than its plan allows (unpublish some first)","tags":["articles"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"savedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"},"revision":{"type":"string","minLength":1,"maxLength":100}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/articles/{slug}/discard-changes":{"post":{"operationId":"articles.discardChanges","summary":"Drop a docs article's unpublished `changes`: it's back to the published version (saved as a new version, so the changes stay in articles.revisions), and Autopilot's edits waiting in them are dismissed. Without changes it does nothing","tags":["articles"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"savedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"},"revision":{"type":"string","minLength":1,"maxLength":100}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/articles/{slug}/unpublish":{"post":{"operationId":"articles.unpublish","summary":"Take a docs article off the portal (back to a draft, with its unpublished changes as the draft's text)","tags":["articles"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/articles/{slug}/revisions":{"get":{"operationId":"articles.revisions","summary":"A docs article's saved versions, newest (the current one) first, with who saved each and its text (the latest 50, or on tuti.so as many as the project's plan includes)","tags":["articles"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/articles/{slug}/restore":{"post":{"operationId":"articles.restore","summary":"Restore a docs article. With `revision` (from articles.revisions), that version becomes the current one, saved as a new version (on a published article, as unpublished `changes`, like any save). Without it, the article most recently deleted with this slug (in the last 30 days) comes back as a draft, under the same slug unless a tab, a collection or another article has taken it (then the next free one). A version that takes out Autopilot's edits waiting in the article's unpublished changes is refused (409, naming them) unless `dismissEdits` lists them (their update ids, given in the 409): then those edits are dismissed with it","tags":["articles"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"revision":{"type":"string","minLength":1,"maxLength":100},"dismissEdits":{"maxItems":1000,"type":"array","items":{"type":"string","minLength":1,"maxLength":100}}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/articles/{slug}/verify":{"post":{"operationId":"articles.verify","summary":"Verify a published docs article against the project's GitHub repository (its default branch at its latest commit): what it says that's still true, out of date or can't be told, with the files that show it, and an Autopilot update fixing what's out of date (it waits for the team on the article's review, codeChanges.target). Checks the article's newest version (its unpublished changes, with every edit waiting in them). Sends repository files (secrets left out) to the AI provider, and costs AI credits where they're used (402 when out). Only while the project analyses merged pull requests and has a repository (409 otherwise). One runs per article at a time: starting it again returns the one running. Run again at the same commit on the same version, it's skipped","tags":["articles"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/articles/{slug}/verifications":{"get":{"operationId":"articles.verifications","summary":"A docs article's verification history (articles.verify), newest first: each run's commit, what it found per statement, and its fix","tags":["articles"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/docs/verify-all":{"post":{"operationId":"articles.verifyAll","summary":"Verify every published docs article (articles.verify on each, one after another, most read first; each its own run, so one failing doesn't stop the rest), with one email to the owners at the end for the fixes it wrote. Only people can start it, in the app, after seeing what it costs: API keys and MCP get 403. One runs per project at a time: starting it again returns the one running, with its progress. With AI credits, running out stops it, saying how far it got (402 when there are none to start with). Articles verified at the same commit and unchanged since are skipped","tags":["articles"],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/imports":{"post":{"operationId":"imports.start","summary":"Start an import from Featurebase with an API key of its own and the parts to bring (contacts: as the project's users, by their id in your app; feedback: boards, posts, votes, comments and the team's private comments as notes, quietly, with their dates; docs: the help center's collections and articles, with their media and the links between them, published as they were). Feedback needs Contacts (400 without). The key is checked, then each part's list read once: a key Featurebase refuses, or one that can't read a part, is refused (400, saying which). It runs in the background, a page at a time (imports.get shows its progress); the key is kept only while it runs. Only people can start it, in the app's first run: API keys and MCP get 403. One runs per project at a time: starting it again returns the one running","tags":["imports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"source":{"type":"string","const":"featurebase"},"key":{"type":"string","minLength":1,"maxLength":500},"parts":{"minItems":1,"type":"array","items":{"type":"string","enum":["contacts","feedback","docs","inbox"]}}},"required":["source","key","parts"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/imports/latest":{"get":{"operationId":"imports.get","summary":"The project's latest import (imports.start), running or ended, with what came across so far, per kind, and what it left out and why (null: none yet)","tags":["imports"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/articles/{slug}/reorder":{"post":{"operationId":"articles.reorder","summary":"Move a docs article within its collection, or into another (`collection`, null for none), at `position` (0 first; left out, last); the others keep their order around it. Readers list articles in this order. A move to another collection goes live at once, on a published article too (its unpublished changes move with it), and is kept as a version","tags":["articles"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"collection":{"anyOf":[{"type":"string","minLength":1,"maxLength":80},{"type":"null"}]},"position":{"type":"integer","minimum":0,"maximum":10000}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/collections":{"get":{"operationId":"collections.list","summary":"List the docs' collections in order (by tab, each top-level one, then the ones in it), with the collection each is in (`parent`), its tab, its icon and how many articles each has","tags":["collections"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"collections.create","summary":"Add a docs collection, at the top level (in `tab`; left out, the first one, if the docs have tabs) or in another collection (`parent`; 5 levels deep at most), after the others there; its slug comes from the name. `access` says who can read it and what's in it, on top of its tab and the collections it's in (`inherit`, the default; `signed_in`; or `groups`, readers in one of `groups`, keys of the project's reader groups (see groups.list), besides the team; `groups` alone, as before, means `groups`); `icon` names a Lucide icon (lucide.dev) shown next to its name. `noAccess` says what people it isn't for see of it and what's in it (`inherit`, the default; `lock`: its name with a lock, nothing in it; or `hide`)","tags":["collections"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":60},"description":{"default":"","type":"string","maxLength":300},"icon":{"type":["string","null"]},"access":{"type":"string","enum":["inherit","signed_in","groups"]},"groups":{"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":50,"pattern":"^[a-z0-9][a-z0-9_.:-]*$"}},"noAccess":{"type":"string","enum":["inherit","lock","hide"]},"parent":{"anyOf":[{"type":"string","minLength":1,"maxLength":80},{"type":"null"}]},"tab":{"type":"string","minLength":1,"maxLength":80}},"required":["name"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/collections/{slug}":{"patch":{"operationId":"collections.update","summary":"Rename a docs collection (its slug follows the name), change its description, move it into another collection (`parent`, null for the top level; it goes last there), to another tab (`tab`, at the top level), set who can read it (`access` and `groups`, as in collections.create; `groups` alone, as before: some make it `groups`, none `inherit`), its `icon` (a Lucide icon's name; null removes it) what people it isn't for see (`noAccess`, as in collections.create), or set its `order` among its siblings. To move it to a place, see collections.reorder","tags":["collections"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":60},"description":{"type":"string","maxLength":300},"access":{"type":"string","enum":["inherit","signed_in","groups"]},"groups":{"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":50,"pattern":"^[a-z0-9][a-z0-9_.:-]*$"}},"noAccess":{"type":"string","enum":["inherit","lock","hide"]},"icon":{"type":["string","null"]},"parent":{"anyOf":[{"type":"string","minLength":1,"maxLength":80},{"type":"null"}]},"tab":{"type":"string","minLength":1,"maxLength":80},"order":{"type":"integer","minimum":0,"maximum":10000}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"delete":{"operationId":"collections.delete","summary":"Delete a docs collection; the collections in it move up a level, in its place, and its articles stay, without a collection","tags":["collections"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/collections/{slug}/reorder":{"post":{"operationId":"collections.reorder","summary":"Move a docs collection among its siblings, into another collection (`parent`, null for the top level; 5 levels deep at most, and not one inside it) or to another tab (`tab`, at the top level; left out, the one it's in, or was in through its parent), at `position` (0 first; left out, last); the others keep their order around it. Readers list collections in this order","tags":["collections"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"parent":{"anyOf":[{"type":"string","minLength":1,"maxLength":80},{"type":"null"}]},"tab":{"type":"string","minLength":1,"maxLength":80},"position":{"type":"integer","minimum":0,"maximum":10000}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/tabs":{"get":{"operationId":"tabs.list","summary":"List the docs' tabs in order, each with its collections and what its path shows (`landing`, `landingPage`; see tabs.update). Without tabs, the docs are one page","tags":["tabs"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"tabs.create","summary":"Add a docs tab, after the others; its slug comes from the name. The first one takes every top-level collection: once there are tabs, each top-level collection is in one. `access` says who can read it (`inherit`, the default: everyone who may read the portal; `signed_in`; or `groups`, with `groups`), and so what's in it; the first tab's is also the articles' in no collection. `noAccess` says what people it isn't for see of it (`inherit`, the default: hidden; `lock`: its name with a lock, nothing in it; or `hide`)","tags":["tabs"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":40},"access":{"type":"string","enum":["inherit","signed_in","groups"]},"groups":{"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":50,"pattern":"^[a-z0-9][a-z0-9_.:-]*$"}},"noAccess":{"type":"string","enum":["inherit","lock","hide"]}},"required":["name"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/tabs/{slug}":{"patch":{"operationId":"tabs.update","summary":"Rename a docs tab (its slug follows the name) or set what its path shows (`landing`): an `overview` (search and a card per top-level collection; the default), its `first` article in reading order, or a `page`: the article or collection in this tab that `landingPage` names (its slug; required with `page`). A picked page a reader can't read there (unpublished, hidden, restricted, moved to another tab) shows them the overview instead. Or change who can read it (`access` and `groups`) or what the rest see (`noAccess`), as in tabs.create","tags":["tabs"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":40},"landing":{"type":"string","enum":["overview","first","page"]},"landingPage":{"type":"string","minLength":1,"maxLength":80},"access":{"type":"string","enum":["inherit","signed_in","groups"]},"groups":{"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":50,"pattern":"^[a-z0-9][a-z0-9_.:-]*$"}},"noAccess":{"type":"string","enum":["inherit","lock","hide"]}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"delete":{"operationId":"tabs.delete","summary":"Delete a docs tab. Its collections go last in the tab before it, with everything in them, and keep who can read them. The first tab is the docs' home (/docs): it can't be deleted while there are others (move another first; 409), and deleting it when it's the only one leaves the docs without tabs","tags":["tabs"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/tabs/{slug}/reorder":{"post":{"operationId":"tabs.reorder","summary":"Move a docs tab to `position` among the tabs (0 first; left out, last); the others keep their order around it. Readers see the tabs in this order, the first one when they open the docs. The articles in no collection keep who can read them when another tab becomes first","tags":["tabs"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"position":{"type":"integer","minimum":0,"maximum":10000}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/groups":{"get":{"operationId":"groups.list","summary":"List the reader groups by name, each with its `key` (how tabs.*, collections.*, articles.* and these paths name it): one of your app's users is in one when their properties match all of its `conditions`; `readers` counts them now and `sample` names up to 3. Tabs, collections and articles can be for some groups only (`access` groups, with `groups`, on tabs.*, collections.* and articles.*)","tags":["groups"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"groups.create","summary":"Add a reader group: its name (its `key` comes from it and never changes), an optional description and who's in it: `conditions` on your users' properties (set by SSO tokens' `properties` and users.sync; up to 10, all must match), each a `property`, an `op` and a `value`: `is`, `is_not`, `contains` or `excludes` a text; `is_any_of` or `is_none_of` a list; `is_set` and `is_not_set` take none. Values compare as text, exactly; a list property matches when one of its items does (or, for the negative ones, none does), and a missing one matches only the negative ones and `is_not_set`. An SSO token's `groups` is the `groups` property, e.g. `{ \"property\": \"groups\", \"op\": \"is\", \"value\": \"beta\" }`","tags":["groups"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":50},"description":{"default":"","type":"string","maxLength":300},"conditions":{"minItems":1,"maxItems":10,"type":"array","items":{"oneOf":[{"type":"object","properties":{"property":{"type":"string","minLength":1,"maxLength":64},"op":{"type":"string","enum":["is","is_not","contains","excludes"]},"value":{"type":"string","minLength":1,"maxLength":200}},"required":["property","op","value"]},{"type":"object","properties":{"property":{"type":"string","minLength":1,"maxLength":64},"op":{"type":"string","enum":["is_any_of","is_none_of"]},"value":{"minItems":1,"maxItems":50,"type":"array","items":{"type":"string","minLength":1,"maxLength":200}}},"required":["property","op","value"]},{"type":"object","properties":{"property":{"type":"string","minLength":1,"maxLength":64},"op":{"type":"string","enum":["is_set","is_not_set"]}},"required":["property","op"]}]}}},"required":["name","conditions"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/groups/{group}":{"patch":{"operationId":"groups.update","summary":"Change a reader group (`group` is its key, which stays): its name, description or `conditions` (all of them, at least one). Then it's whichever of your users match them, and the tabs, collections and articles for it follow","tags":["groups"],"parameters":[{"name":"group","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":50,"pattern":"^[a-z0-9][a-z0-9_.:-]*$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":50},"description":{"type":"string","maxLength":300},"conditions":{"minItems":1,"maxItems":10,"type":"array","items":{"oneOf":[{"type":"object","properties":{"property":{"type":"string","minLength":1,"maxLength":64},"op":{"type":"string","enum":["is","is_not","contains","excludes"]},"value":{"type":"string","minLength":1,"maxLength":200}},"required":["property","op","value"]},{"type":"object","properties":{"property":{"type":"string","minLength":1,"maxLength":64},"op":{"type":"string","enum":["is_any_of","is_none_of"]},"value":{"minItems":1,"maxItems":50,"type":"array","items":{"type":"string","minLength":1,"maxLength":200}}},"required":["property","op","value"]},{"type":"object","properties":{"property":{"type":"string","minLength":1,"maxLength":64},"op":{"type":"string","enum":["is_set","is_not_set"]}},"required":["property","op"]}]}}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"delete":{"operationId":"groups.delete","summary":"Delete a reader group (by key). One a tab, collection or article is for is refused (409): taking it away would change who can read them, so remove it from them first. Deleted articles let it go","tags":["groups"],"parameters":[{"name":"group","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":50,"pattern":"^[a-z0-9][a-z0-9_.:-]*$"}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/users":{"get":{"operationId":"users.list","summary":"List the project's users (the people your app signs in or sends with users.sync) by name, each with their `id` in your app, email, properties, the reader groups they're in now and their `activity` (posts, votes, conversations, when last seen); `q` finds them by name, email or id (whoever has that id first), `group` (a key) keeps those in that group, `company` those at that company (their `company` property), and `sort` puts the most posts, votes or conversations, or the latest seen, first","tags":["users"],"parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","maxLength":255}},{"name":"group","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":50,"pattern":"^[a-z0-9][a-z0-9_.:-]*$"}},{"name":"company","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":200}},{"name":"sort","in":"query","required":false,"schema":{"default":"name","type":"string","enum":["name","posts","votes","conversations","lastSeen"]}},{"name":"limit","in":"query","required":false,"schema":{"default":50,"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"schema":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"users.sync","summary":"Add or change the project's users, up to 1000 at once, each by their `id` in your app (the `sub` of your SSO tokens): their name, email, avatar and properties, which reader groups' conditions match. Only what's given changes; `properties` sets the ones it names (strings, numbers, booleans or lists of strings) and removes the ones that are null, keeping the rest, up to 50 per user. Someone signed in to your portal with a verified address is in the groups of the users with that email. All or nothing; `created` and `updated` say how many","tags":["users"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"users":{"minItems":1,"maxItems":1000,"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":255},"name":{"type":"string","minLength":1,"maxLength":100},"email":{"anyOf":[{"type":"string","maxLength":320,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]},"avatar":{"anyOf":[{"type":"string","maxLength":2048,"format":"uri"},{"type":"null"}]},"properties":{"type":"object","propertyNames":{"type":"string","minLength":1,"maxLength":64},"additionalProperties":{"anyOf":[{"type":"string","maxLength":200},{"type":"number"},{"type":"boolean"},{"maxItems":50,"type":"array","items":{"type":"string","maxLength":200}},{"type":"null"}]}}},"required":["id"]}}},"required":["users"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/companies":{"get":{"operationId":"companies.list","summary":"List the companies your users are at (their `company` property): how many users each has, their `votes` (on others' posts), and with Who's asking picked (project.update's `askingProperty`), the `total` of that number property over them; the most users first, or with `sort`, the most votes or the biggest total. users.list's `company` lists a company's users","tags":["companies"],"parameters":[{"name":"sort","in":"query","required":false,"schema":{"default":"users","type":"string","enum":["users","votes","total"]}},{"name":"limit","in":"query","required":false,"schema":{"default":50,"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"schema":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/users/{user}":{"get":{"operationId":"users.get","summary":"One of the project's users, by their id in your app: name, email, properties and the reader groups they're in now","tags":["users"],"parameters":[{"name":"user","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":255}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/posts":{"get":{"operationId":"posts.list","summary":"List posts, most voted or newest first; merged duplicates are left out","tags":["posts"],"parameters":[{"name":"board","in":"query","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["open","under_review","planned","in_progress","completed","closed"]}},{"name":"sort","in":"query","required":false,"schema":{"default":"top","type":"string","enum":["top","new"]}},{"name":"limit","in":"query","required":false,"schema":{"default":25,"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"schema":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"posts.create","summary":"Create a post, as the key's creator or on behalf of someone from your app (`author`)","tags":["posts"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"board":{"type":"string","minLength":1},"title":{"type":"string","minLength":3,"maxLength":120},"body":{"type":"string","maxLength":10000},"content":{},"author":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":255},"name":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"required":["id"]}},"required":["board","title"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/posts/{number}":{"get":{"operationId":"posts.get","summary":"Get a post by its number, with its text and rich content and `eta`: the month the team expects it (the portal hides it once it has passed or the post is completed or closed); to the team, who looks after it (`owner`)","tags":["posts"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"patch":{"operationId":"posts.update","summary":"Change a post's status, move it to another board, or say who on the team looks after it (`owner`: a team member's email, null for nobody) and when it's expected (`eta`: a month like 2026-12, null for not said)","tags":["posts"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["open","under_review","planned","in_progress","completed","closed"]},"board":{"type":"string"},"owner":{"anyOf":[{"type":"string","format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]},"eta":{"anyOf":[{"type":"string","pattern":"^20\\d{2}-(0[1-9]|1[0-2])$"},{"type":"null"}]}},"required":[],"anyOf":[{"required":["status"]},{"required":["board"]},{"required":["owner"]},{"required":["eta"]}]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/posts/{number}/comments":{"get":{"operationId":"comments.list","summary":"List a post's comments, oldest first: to the team (an API key, a team member) with its internal notes (`internal: true`)","tags":["comments"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"comments.create","summary":"Comment on a post, as the key's creator (shown as the team) or on behalf of someone from your app; or, as the team, leave an internal note (`internal: true`): only the team sees it, never on the portal, in emails or webhooks. The team's reply can be emailed to the post's voters too (`emailVoters: true`)","tags":["comments"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","maxLength":10000},"content":{},"author":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":255},"name":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"required":["id"]},"internal":{"type":"boolean"},"emailVoters":{"type":"boolean"}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/posts/{number}/merge":{"post":{"operationId":"posts.merge","summary":"Merge a duplicate into another post: votes and comments move over, and the duplicate leaves every list","tags":["posts"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"into":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["into"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/posts/{number}/links/{id}":{"delete":{"operationId":"links.remove","summary":"Remove a link from a post","tags":["links"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/code-changes":{"get":{"operationId":"codeChanges.list","summary":"List code changes (Autopilot): merged pull requests, each with the updates it suggests (those with updates waiting first)","tags":["codeChanges"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/code-changes/{id}":{"get":{"operationId":"codeChanges.get","summary":"Get a code change with its proposed actions","tags":["codeChanges"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/code-change-targets":{"get":{"operationId":"codeChanges.targets","summary":"Autopilot's review by what it updates: the posts (`feedback`) and articles (`helpCenter`: edits, and new articles' drafts) with updates waiting for the team or going out by themselves, the ones that reach the most people first (`reach`: a post's author, voters, commenters and subscribers; an article's views in 30 days). With `view: decided`, those whose updates were all decided, the latest first, each with its `outcome` (what became of it: `completed`, `in_progress`, `published`, `approved` (an edit kept unpublished), `drafted`, `linked`, `dismissed` or `undone`; who decided it, as `line`; and `at`, when). Each has its `badge` (`completing`, `in_progress`, `updated`, `new`, from its updates waiting or applied; null once all were dismissed or undone), the pull requests behind it, its `group` (where it is: a post's board, an article's collection with its `icon`; null for an article in none or a new article's draft), and its `path` for codeChanges.target. The changelog line isn't here: it's written into the running draft by itself","tags":["codeChanges"],"parameters":[{"name":"view","in":"query","required":false,"schema":{"default":"waiting","type":"string","enum":["waiting","decided"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/code-change-targets/{type}/{id}":{"get":{"operationId":"codeChanges.target","summary":"Get a post's or an article's review in Autopilot (`type`: feedback or articles; `id`: the post's id, or the article's, or for a new article's draft not written yet, its update's id): the pull requests behind it and every update on it, waiting or decided; for a post, its `update` waiting: each pull request's part as one status change with one comment, and `asked`: what its users asked (its description's first lines as `excerpt`, its first voters of `voteCount`, its Inbox `conversations`); for a new article's draft, its `place`: where approving saves it (an existing `collection`, or a `newCollection` approving makes, readers seeing it once an article in it is published; after `afterArticle`, or `first`, else last; null: in no collection); and `also`: what the pull requests behind its waiting updates update elsewhere that still waits (each with its pull request's `number`, its review's `path`, its `title`, `kind`, and `stale` when it's out of date)","tags":["codeChanges"],"parameters":[{"name":"type","in":"path","required":true,"schema":{"type":"string","enum":["feedback","articles"]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/code-changes/{id}/analyse":{"post":{"operationId":"codeChanges.analyse","summary":"Analyse a code change again; one with nothing to do is analysed past that decision (\"Analyse anyway\"); one already being analysed (`state` analysing, for up to 30 minutes) isn't queued again; refused (402) out of AI credits or where the project's plan doesn't include code changes","tags":["codeChanges"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/code-changes/resume":{"post":{"operationId":"codeChanges.resume","summary":"Resume every paused code change, paused because the project was out of AI credits or its plan didn't include code changes (\"Resume all\"): each is analysed again (`resumed`: queued, or where jobs run inline, analysed; `failed`: those whose analysis failed at once, inline); refused (402) while the project is still out, or its plan still doesn't include them","tags":["codeChanges"],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/code-change-actions/{id}/apply":{"post":{"operationId":"codeChangeActions.apply","summary":"Apply a proposed action: link the post, complete it (its people get the shipped email) or post the team's comment on it (a post's update is every pull request's part waiting on it, applied as one: one status change, one comment theirs joined (`comment` replaces it), each pull request linked; `revision`: only if it's still the update reviewed, codeChanges.target's `update.revision`); approve an article's edit, which publishes the article's unpublished changes with every edit waiting in them (`revision`: only if it's still the version reviewed; on a draft, its edits are approved and stay in the draft, unpublished, `published: false`, and only with `reviewedAs: \"draft\"` and its `revision`, else a 409 `draft`); create a new article's draft (in its `place`, a new collection made for it, readers seeing that only once an article in it is published; `collection` puts it in another, or none); or write the pull request's line in the changelog (its bullet in the entry that has it, else the running draft)","tags":["codeChangeActions"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"comment":{"type":"string","maxLength":5000},"line":{"type":"string","minLength":1,"maxLength":300},"revision":{"type":"string","minLength":1,"maxLength":100},"reviewedAs":{"type":"string","enum":["published","draft"]},"collection":{"anyOf":[{"type":"string","minLength":1,"maxLength":80},{"type":"null"}]},"publish":{"description":"Gone: refused if sent","not":{}}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/code-change-actions/apply":{"post":{"operationId":"codeChangeActions.applyMany","summary":"Apply several proposed actions at once (the review page's Approve), each as codeChangeActions.apply does, with its edits (`comment`, `line`) and the version reviewed (`revision`): in order, each on its own, so one refused (out of date, already decided) doesn't stop the rest. Each is claimed as it's applied, so two people approving the same ones at once apply each once. `results` says, for each, `applied` or why not","tags":["codeChangeActions"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"actions":{"minItems":1,"maxItems":50,"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":100},"comment":{"type":"string","maxLength":5000},"line":{"type":"string","minLength":1,"maxLength":300},"revision":{"type":"string","minLength":1,"maxLength":100},"publish":{"description":"Gone: refused if sent","not":{}}},"required":["id"]}}},"required":["actions"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/code-change-actions/{id}/undo":{"post":{"operationId":"codeChangeActions.undo","summary":"Undo an applied action (any team member): a post goes back to the status it had (shipped emails still due are cancelled; ones sent already can't be), a progress update's comment is deleted, the link is removed (a post's update applied as one, from several pull requests, is undone whole: every part's links too), an article's edit is taken out by section, of the published version (published again) and of its unpublished changes (of a draft's text, on a draft), everything else staying (refused when an edit on top of it changed the same sections; one approved before edits were saved as revisions publishes the previous version again, refused if it's been edited since), a new article's draft is deleted (refused once edited or published), the changelog line is put back (refused once its entry is published: edit the entry). It becomes `undone` and isn't proposed again","tags":["codeChangeActions"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/code-change-actions/{id}/publish-now":{"post":{"operationId":"codeChangeActions.publishNow","summary":"Publish now: a scheduled action (going out by itself after the hold) is applied at once, as codeChangeActions.apply does, by whoever asks","tags":["codeChangeActions"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/code-change-actions/{id}/stop":{"post":{"operationId":"codeChangeActions.stop","summary":"Stop: a scheduled action doesn't go out by itself; it waits for the team instead (to approve, edit or dismiss). A post's part stops all of its post's update (it goes out as one)","tags":["codeChangeActions"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/code-change-actions/{id}/dismiss":{"post":{"operationId":"codeChangeActions.dismiss","summary":"Dismiss a proposed action, with an optional reason (it isn't proposed again for this code change). A post's part, waiting or going out by itself, takes only its part out of the post's update; with `revision` (the post's update's version, as `apply` takes it), every part of that update is dismissed at once, refused (409) when it changed since. An article's edit is taken out of the article's unpublished changes by section, everything else in them staying; refused (409) when an edit on top of it changed the same sections","tags":["codeChangeActions"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":500},"revision":{"type":"string","minLength":1,"maxLength":100}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/posts/{number}/vote":{"post":{"operationId":"votes.set","summary":"Vote for a post or take the vote back, as the key's creator or on behalf of someone from your app","tags":["votes"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"voted":{"type":"boolean"},"author":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":255},"name":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"required":["id"]}},"required":["voted"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/posts/{number}/subscription":{"post":{"operationId":"subscriptions.set","summary":"Follow a post without voting for it, or stop: its followers are emailed when it ships, as its voters are","tags":["subscriptions"],"parameters":[{"name":"number","in":"path","required":true,"schema":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"subscribed":{"type":"boolean"},"author":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":255},"name":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"required":["id"]}},"required":["subscribed"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/email-preferences":{"patch":{"operationId":"emailPreferences.update","summary":"Choose which of the project's emails about posts someone gets, as the key's creator or on behalf of someone from your app: about posts they wrote (`ownPosts`) or posts they voted on, commented on or follow (`followedPosts`), each when it ships, and the team's replies emailed to its voters (writing a post votes for it); or no emails about posts at all, nor the changelog's to their address (`unsubscribed`, as the unsubscribe link in every such email; replies to their messages and sign-in emails still arrive). The changelog entries emailed to subscribers, at the address the project's emails go to (`changelog`: on confirms it at once; off is the unsubscribe link in every changelog email; a private portal emails none). What's left out stays as it was. On someone's behalf, only turning emails off. `unsubscribed: true` and `changelog` need an address they've proven (verified, or from your single sign-on); `changelog: true` is refused while emails to that address are turned off (`unsubscribed`, or another account's at the same address; `unsubscribed: false` in the same update counts; a refused update changes nothing)","tags":["emailPreferences"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ownPosts":{"type":"boolean"},"followedPosts":{"type":"boolean"},"unsubscribed":{"type":"boolean"},"changelog":{"type":"boolean"},"author":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":255},"name":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","maxLength":320,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"required":["id"]}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/duplicates":{"get":{"operationId":"duplicates.list","summary":"List possible duplicates waiting for the team: a newer post that may repeat an older one","tags":["duplicates"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/duplicates/{id}/dismiss":{"post":{"operationId":"duplicates.dismiss","summary":"Mark a possible duplicate as not a duplicate (it won't be suggested again)","tags":["duplicates"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/widget":{"get":{"operationId":"widget.get","summary":"Get the widget's settings: the launcher's name (label), colour, corner and icon, and the panel's Home: quick links, heading (title), greeting and the parts turned off (hidden), and whether the launcher shows on the project's own portal (onPortal)","tags":["widget"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"patch":{"operationId":"widget.update","summary":"Set the widget's launcher, a round button: its name (label: read out by screen readers and its tooltip), colour (#rrggbb, null for the brand green), corner, up to 5 quick links (replaces the list), and its icon (chat, help, megaphone, lightbulb, image; \"image\" with iconImage, the id of an image uploaded in the app). Where plans are sold, an icon other than chat needs Pro (402 otherwise); on a lower plan the one set is kept and the launcher shows chat. The panel's Home: its heading (title) and the greeting above it (empty for \"How can we help?\" and \"Hi there 👋\", with the visitor's first name once their site signs them in), and the parts turned off (hidden, replaces the list: docs, messages, feedback, whatsNew), gone from Home and the tabs. onPortal: the launcher on every page of the project's own portal too (true for new projects)","tags":["widget"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":30},"color":{"anyOf":[{"type":"string","pattern":"^#[0-9a-f]{6}$"},{"type":"null"}]},"position":{"type":"string","enum":["bottom-right","bottom-left"]},"links":{"maxItems":5,"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":40},"url":{"type":"string","maxLength":500}},"required":["label","url"]}},"icon":{"type":"string","enum":["chat","help","megaphone","lightbulb","image"]},"iconImage":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"title":{"type":"string","maxLength":60},"greeting":{"type":"string","maxLength":60},"hidden":{"type":"array","items":{"type":"string","enum":["docs","messages","feedback","whatsNew"]}},"onPortal":{"type":"boolean"}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/branding":{"get":{"operationId":"branding.get","summary":"Get the portal's branding: logos, favicon, colour, header and footer links, and whether it says \"Powered by tuti\"","tags":["branding"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"patch":{"operationId":"branding.update","summary":"Set the portal's branding: logo, logoDark and favicon (ids of images uploaded in the app, null to remove), colour (#rrggbb, null for the brand green), up to 5 https links each for the header and footer (replaces the list), poweredBy (false needs a licence for the project), contentActions (what readers can do with an article: the \"Copy page\" menu at all, copyMarkdown, chatGpt and claude in it, and the \"Updated\" date; each true or false), docsHome (the docs' home: a title up to 80 characters and a subtitle up to 200 at its top, empty for the defaults, and popular to list each tab's most-read articles on its overview, once there are three, on for new projects), colorScheme (system, light or dark: for readers who haven't picked one), logoText (up to 40 characters beside the logo), logoLink (https, null for the portal's home), up to 5 sidebarLinks above the docs' tree, seo (the portal's search listing: a title up to 70 characters, empty for the project's name, and a description up to 160), seoImage (an uploaded image for links to the portal, null to remove) and fonts (heading and body: one of inter, roboto, open-sans, lato, montserrat, poppins, source-sans-3, nunito, work-sans, dm-sans, manrope, ibm-plex-sans, space-grotesk, merriweather, lora, playfair-display, or null for the page's own), customCss (CSS on every portal page) and customHtml (HTML, scripts too, at the end of each page only where the portal is on your own domain or a path of your site: never on its Tuti address, in the widget or on the app's own domain); each up to 50000 characters. Fields left out stay as they are","tags":["branding"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"logo":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"logoDark":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"favicon":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"color":{"anyOf":[{"type":"string","pattern":"^#[0-9a-f]{6}$"},{"type":"null"}]},"headerLinks":{"maxItems":5,"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":40},"url":{"type":"string","maxLength":500}},"required":["label","url"]}},"footerLinks":{"maxItems":5,"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":40},"url":{"type":"string","maxLength":500}},"required":["label","url"]}},"poweredBy":{"type":"boolean"},"contentActions":{"type":"object","properties":{"menu":{"type":"boolean"},"copyMarkdown":{"type":"boolean"},"chatGpt":{"type":"boolean"},"claude":{"type":"boolean"},"updatedDate":{"type":"boolean"}}},"docsHome":{"type":"object","properties":{"title":{"type":"string","maxLength":80},"subtitle":{"type":"string","maxLength":200},"popular":{"type":"boolean"}}},"colorScheme":{"type":"string","enum":["system","light","dark"]},"logoText":{"type":"string","maxLength":40},"logoLink":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}]},"sidebarLinks":{"maxItems":5,"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":40},"url":{"type":"string","maxLength":500}},"required":["label","url"]}},"seo":{"type":"object","properties":{"title":{"type":"string","maxLength":70},"description":{"type":"string","maxLength":160}}},"seoImage":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"fonts":{"type":"object","properties":{"heading":{"anyOf":[{"type":"string","enum":["inter","roboto","open-sans","lato","montserrat","poppins","source-sans-3","nunito","work-sans","dm-sans","manrope","ibm-plex-sans","space-grotesk","merriweather","lora","playfair-display"]},{"type":"null"}]},"body":{"anyOf":[{"type":"string","enum":["inter","roboto","open-sans","lato","montserrat","poppins","source-sans-3","nunito","work-sans","dm-sans","manrope","ibm-plex-sans","space-grotesk","merriweather","lora","playfair-display"]},{"type":"null"}]}}},"customCss":{"type":"string","maxLength":50000},"customHtml":{"type":"string","maxLength":50000}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/license/check":{"post":{"operationId":"license.check","summary":"Check tuti.so for the project's licence now (bought at tuti.so/license): status licensed, not_licensed (none yet, or cancelled), in_use (on another installation), unreachable (tuti.so didn't answer; the current key stays) or busy (just checked), and licensedUntil","tags":["license"],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/usage":{"get":{"operationId":"usage.get","summary":"What the project's AI use cost, by calendar month (UTC, newest first) and feature: requests, tokens in and out, and the cost in dollars (`unpriced`: requests whose provider gave no price); on a server with AI credits, `credits` charged instead of the cost","tags":["usage"],"parameters":[{"name":"months","in":"query","required":false,"schema":{"default":12,"type":"integer","minimum":1,"maximum":24}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/ai-models":{"get":{"operationId":"aiModels.get","summary":"Get the model each AI feature uses (answers, escalations, support, changelog, code_changes: Autopilot's writing and Verify; code_changes_decide and code_changes_redecide: its docs agent deciding and double-checking): the one the project picked (null for the server's default), and the default","tags":["aiModels"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"patch":{"operationId":"aiModels.update","summary":"Pick the model an AI feature uses for this project: a model id the AI endpoint lists (OpenRouter's, e.g. anthropic/claude-sonnet-5.5), or \"\" for the server's default; \"off\" for code_changes_redecide skips Autopilot's double-check. Autopilot's agent steps take only models with tool calls. The server may cap a model's price (AI_MODEL_MAX_PRICE). Features left out keep theirs","tags":["aiModels"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"answers":{"type":"string","maxLength":200},"escalations":{"type":"string","maxLength":200},"changelog":{"type":"string","maxLength":200},"code_changes":{"type":"string","maxLength":200},"support":{"type":"string","maxLength":200},"code_changes_decide":{"type":"string","maxLength":200},"code_changes_redecide":{"type":"string","maxLength":200}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/code-changes-tuning":{"get":{"operationId":"codeChangesTuning.get","summary":"Get Autopilot's Advanced tuning: for each value (propose.edit.maybe (Fix an article from), propose.newArticle.maybe (Write a new article from), redecide.whenNewArticle (Double-check a new article from), decide.rounds (Lookups before deciding), write.rounds (Lookups before writing), decide.reasoning (Thinking), context.diffChars (Change shown up front)) what analyses use, the server's default, the project's own (null for none) and its range; whether the plan includes it (Business), and who changed it last","tags":["codeChangesTuning"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"patch":{"operationId":"codeChangesTuning.update","summary":"Change Autopilot's Advanced tuning (owners only, on the Business plan): any of its values, each inside the server's range (see codeChangesTuning.get), or null for the server's. Values left out keep theirs","tags":["codeChangesTuning"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"propose.edit.maybe":{"anyOf":[{"type":"number"},{"type":"string","maxLength":20},{"type":"null"}]},"propose.newArticle.maybe":{"anyOf":[{"type":"number"},{"type":"string","maxLength":20},{"type":"null"}]},"redecide.whenNewArticle":{"anyOf":[{"type":"number"},{"type":"string","maxLength":20},{"type":"null"}]},"decide.rounds":{"anyOf":[{"type":"number"},{"type":"string","maxLength":20},{"type":"null"}]},"write.rounds":{"anyOf":[{"type":"number"},{"type":"string","maxLength":20},{"type":"null"}]},"decide.reasoning":{"anyOf":[{"type":"number"},{"type":"string","maxLength":20},{"type":"null"}]},"context.diffChars":{"anyOf":[{"type":"number"},{"type":"string","maxLength":20},{"type":"null"}]}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/credits":{"get":{"operationId":"credits.get","summary":"The project's AI credits, on a server that uses them (`enabled`): the balance (below 0 when a request ran past it; the next credits cover that first) and each grant with credits left, soonest-expiring first (`expiresAt` null: for good)","tags":["credits"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/credits/top-up":{"post":{"operationId":"credits.topUp","summary":"Buy AI credits: a Stripe Checkout page (`url`) to pay `dollars` (5 to 1000), and how many `credits` it gives; they're added once Stripe says it's paid. Off (503) where top-ups aren't set up","tags":["credits"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"dollars":{"type":"integer","minimum":5,"maximum":1000}},"required":["dollars"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/plan":{"get":{"operationId":"plan.get","summary":"The project's plan, on a server that sells plans (`enabled`): `free`, `pro` or `business`, and for a paid plan how it's billed (`interval`: month or year), Stripe's `status` (`past_due` while a failed payment is retried), when its period ends (`periodEnd`: it renews then), if it's cancelled, when it ends (`endsAt`), and whether it has the add-on hiding \"Powered by tuti\" (`badge`; see plan.badge)","tags":["plan"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/plan/subscribe":{"post":{"operationId":"plan.subscribe","summary":"Start a paid plan (`pro` or `business`, billed each `month` or `year`): a Stripe Checkout page (`url`) to pay on; the plan starts once Stripe says it's paid. Refused (409) while the project has a plan (change it with plan.manage), and off (503) where plans aren't sold","tags":["plan"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string","enum":["pro","business"]},"interval":{"default":"month","type":"string","enum":["month","year"]}},"required":["plan"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/plan/manage":{"post":{"operationId":"plan.manage","summary":"Stripe's customer portal for the project (`url`): change or cancel its plan, update the card, and download invoices. Refused (409) for a project that has never had a plan","tags":["plan"],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/plan/badge":{"post":{"operationId":"plan.badge","summary":"The add-on hiding \"Powered by tuti\" on the portal ($49 a month, on a paid plan): `on` adds it to the plan, charging what's left of the period at once, and hides the badge; `on: false` removes it (what's left of the period is credited to the next invoice) and shows the badge again. It ends with the plan. Adding is refused (409) on the free plan or while a payment is failing, and 402 if the card is declined; removing always works (on the free plan there's nothing to remove: `{ on: false }`)","tags":["plan"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"on":{"type":"boolean"}},"required":["on"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/portal":{"get":{"operationId":"portal.get","summary":"Get who may read the portal: public (anyone) or private (only the team, people your app signs in with SSO and readers added by email); and what its home page opens: feedback (the first board), roadmap, changelog or docs","tags":["portal"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"patch":{"operationId":"portal.update","summary":"Set who may read the portal: public (anyone), or private (only the team, people your app signs in with SSO and readers added by email, see portalReaders.add; its pages, feeds, widget and AI answers ask everyone else to sign in; where plans are sold, on Business: 402 otherwise, and on a lower plan a private portal is its team's only); and what its home page opens (`home`: feedback, its first board; roadmap; changelog; or docs)","tags":["portal"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"access":{"type":"string","enum":["public","private"]},"home":{"type":"string","enum":["feedback","roadmap","changelog","docs"]}},"required":[]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/portal/readers":{"get":{"operationId":"portalReaders.list","summary":"List the people (by email) who may read the portal while it's private, besides the team and people your app signs in with SSO; newest first","tags":["portalReaders"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"portalReaders.add","summary":"Let people read the portal while it's private, by email (up to 20 at once, 2000 in all): any account with that address, once it's verified (an emailed sign-in link verifies it). An entry can be a whole domain (\"@acme.com\"): everyone with a verified address there (not free email providers, where anyone can get one). On a private portal, each new one is emailed a link to it unless `notify` is false (up to 100 such emails a day per portal, and 3 per address; past them, readers are added without one, and `emailed` says how many were sent). Addresses already there are left as they are","tags":["portalReaders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"emails":{"minItems":1,"maxItems":20,"type":"array","items":{"type":"string"}},"notify":{"default":true,"type":"boolean"}},"required":["emails"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/portal/readers/{email}":{"delete":{"operationId":"portalReaders.remove","summary":"Stop letting someone (an address) or a domain (\"@acme.com\") read the private portal (the team and SSO users keep their access); accounts stay","tags":["portalReaders"],"parameters":[{"name":"email","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":320}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/domain":{"get":{"operationId":"domain.get","summary":"Get the portal's custom domain (null if none): its hostname, whether it's verified, and the DNS records to add (a CNAME pointing it at Tuti, and a TXT record that proves it's yours)","tags":["domain"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"domain.set","summary":"Use your own domain for the portal and its docs (e.g. help.example.com; one per project, replacing any other; not while the portal is under a path of your site, see site.set: remove that first): add the DNS records it returns, then domain.verify. Until then the portal stays where it is. Where plans are sold, on Pro and Business (402 otherwise; on a lower plan a domain set before isn't served, and the portal is at its Tuti address)","tags":["domain"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"hostname":{"type":"string","maxLength":300}},"required":["hostname"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"delete":{"operationId":"domain.remove","summary":"Stop using the custom domain: the portal answers only at its Tuti address again","tags":["domain"],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/domain/verify":{"post":{"operationId":"domain.verify","summary":"Check the custom domain's TXT record (see domain.get): once it's there, the domain is verified and the portal answers on it (where Tuti gets certificates through Cloudflare, `certificate` says when HTTPS works there: pending until the CNAME points at Tuti, then active). DNS changes can take a few minutes to show","tags":["domain"],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/site":{"get":{"operationId":"site.get","summary":"Get where the portal is on your own site (null if nowhere): its address (e.g. https://example.com/portal), whether it's verified, what your site must forward, and the check Tuti makes","tags":["site"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"site.set","summary":"Put the portal under a path of your own site (e.g. https://example.com/portal; one per project, replacing any other; not while the portal has a custom domain, see domain.set: remove that first): make your site forward everything under that path, and /_tuti (the portal's scripts and styles), to the portal's Tuti address, paths and all (`forward`), then site.verify. Where plans are sold, on Business (402 otherwise; on a lower plan a path set before isn't served, and the portal is at its Tuti address)","tags":["site"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","maxLength":500}},"required":["url"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"delete":{"operationId":"site.remove","summary":"Stop putting the portal under your site's path: it answers at its own address again","tags":["site"],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/site/verify":{"post":{"operationId":"site.verify","summary":"Check that your site forwards the portal's path to Tuti (Tuti fetches <address>/.well-known/tuti-site through it and expects this project's token): once it does, the portal answers there and every link Tuti makes uses that address","tags":["site"],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/redirects":{"get":{"operationId":"redirects.list","summary":"List the portal's redirects, newest first: each sends requests for `from` (a path of the portal) to `to` (a path of it, or a whole https address)","tags":["redirects"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"redirects.create","summary":"Send an old address on the portal on to a new one: `from` is the old path (or a whole URL of the old site, whose path is taken), matched lowercase whatever the query; `to` is a path of the portal or a whole https address, which gets the request's query unless it has its own. Not the portal's own addresses (sign-in, /api, /widget, …) or its home, one rule per `from`, no rule to or from another rule's address (so they can't go round in circles), up to 200","tags":["redirects"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string","maxLength":2000},"to":{"type":"string","maxLength":2000}},"required":["from","to"]}}}},"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/redirects/{id}":{"delete":{"operationId":"redirects.delete","summary":"Stop a redirect (`id`, from redirects.list): its old address is the portal's again","tags":["redirects"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"description":"Needs a read + write key.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"A project API key (tuti_…)"}},"responses":{"Error":{"description":"An error: 400 bad input, 401 missing or revoked key, 403 read-only key, 404 not found, 409 conflict, 429 rate limited, 502 a service Tuti uses failed (e.g. the AI model), 503 a feature isn't set up on this server","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}}