tuti

API

Read and change your project's feedback from your own code. Base URL https://tuti.so/api/v1. The full description is at openapi.json (OpenAPI 3.1).

Keys, limits and errors

  • Create a key in your project's Settings → Developers and send it as Authorization: Bearer tuti_…. Keep it on your server. A key works for one project.
  • Read only keys can use the GET endpoints; changing anything needs a Read and write key.
  • Up to 600 requests a minute per key; over that you get 429 with a Retry-After header.
  • Errors are { "error": { "code", "message" } } with status 400, 401, 403, 404, 409 or 429.
  • Posts and comments are written by the key's creator, or on behalf of someone from your app with "author": { "id", "name", "email" }: their id in your app, the same person as with single sign-on.

MCP (for AI assistants)

Every endpoint below is also a tool on Tuti's MCP server, so assistants like Claude can search, triage and answer your feedback. Add it as a remote MCP server (Streamable HTTP) at https://tuti.so/mcp with the header Authorization: Bearer tuti_…. Read only keys get only the reading tools.

claude mcp add --transport http tuti https://tuti.so/mcp \
  --header "Authorization: Bearer $TUTI_API_KEY"

Webhooks

Add endpoints in Settings → Developers. Tuti sends a POST with a JSON body for each event you pick: post.created (new post), post.status_changed (status changed), comment.created (new comment), changelog.published (changelog published), code_change.ready (autopilot found updates), conversation.created (new conversation), conversation.message_created (new message from a user), plus ping from "Send test".

  • Headers: Tuti-Event-Id, Tuti-Event-Type and Tuti-Signature.
  • Answer with any 2xx within 10 seconds. Anything else is retried with growing gaps for about a day, always with the same event id: deliveries are at least once, so skip events whose id you've already handled.
  • Redirects aren't followed, and endpoints must be public HTTPS URLs.
{
  "id": "evt_…",
  "type": "post.created",
  "createdAt": "2026-10-19T09:30:00.000Z",
  "project": { "slug": "acme", "name": "Acme" },
  "data": { "number": 12, "title": "Dark mode", "status": "open", … }
}

Check the signature with the webhook's secret before trusting a request (Node):

import { createHmac, timingSafeEqual } from "node:crypto";

// body: the raw request body as a string, before parsing it.
function verify(body, header, secret) {
  const { t, v1 } = Object.fromEntries((header ?? "").split(",").map((part) => part.split("=")));
  if (!/^\d+$/.test(t ?? "") || !/^[0-9a-f]{64}$/.test(v1 ?? "")) return false; // missing or malformed
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // older than 5 minutes
  const expected = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
  return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

GET/api/v1/project

Get the project the key belongs to, with its portal URL and what the team said it will use (`uses`, project.update's).

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/project"

PATCH/api/v1/project

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). Needs a read and write key.

NameInType
namebodystring
askingPropertybodydocument
userColumnsbodyarray
propertyFormatsbodyobject
usesbodyarray
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/project" \
  -H "Content-Type: application/json" \
  -d '{"name":"Acme","askingProperty":"mrr","userColumns":["company","plan","mrr"],"propertyFormats":{"mrr":{"label":"MRR","format":"money","currency":"USD"}},"uses":["feedback","changelog"]}'

PATCH/api/v1/project/property-formats

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. Needs a read and write key.

NameInType
formats *bodyobject
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/project/property-formats" \
  -H "Content-Type: application/json" \
  -d '{"formats":{"mrr":{"label":"MRR","format":"money","currency":"EUR"},"seats":{"label":""}}}'

GET/api/v1/team

List the project's team: names and roles.

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/team"

GET/api/v1/conversations

List conversations, latest message first; spam is left out unless asked for. `snoozed` true lists only those snoozed now, false leaves them out.

NameInType
statusqueryopen | waiting | closed
assigneequerydocument
userquerystring
spamquerystring
snoozedquerystring
limitqueryinteger (1–100)
cursorqueryinteger
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/conversations?status=open&assignee=none&limit=10"

GET/api/v1/conversations/{number}

Get a conversation by its number, with its messages, oldest first.

NameInType
numberpathinteger
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/conversations/1"

POST/api/v1/conversations

Start a conversation from someone in your app (`user`, by their id there) or an email address, with their first message. Needs a read and write key.

NameInType
userbodyobject
emailbodystring
subjectbodystring
body *bodystring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/conversations" \
  -H "Content-Type: application/json" \
  -d '{"user":{"id":"user-42","name":"Sam","email":"[email protected]"},"subject":"Can't export to CSV","body":"The export button does nothing on the reports page."}'

POST/api/v1/conversations/{number}/messages

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). Needs a read and write key.

NameInType
numberpathinteger
body *bodystring
authorbodyobject
notebodyboolean
closebodyboolean
seenbodystring
attachmentsbodyarray
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/conversations/1/messages" \
  -H "Content-Type: application/json" \
  -d '{"body":"Thanks, Sam! A fix is on its way."}'

PATCH/api/v1/conversations/{number}

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. Needs a read and write key.

NameInType
numberpathinteger
statusbodyopen | waiting | closed
assigneebodydocument
spambodyboolean
snoozedUntilbodydocument
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/conversations/1" \
  -H "Content-Type: application/json" \
  -d '{"status":"closed"}'

POST/api/v1/conversations/{number}/posts

Link a conversation to a post it's about; merging the post takes the link along. Needs a read and write key.

NameInType
numberpathinteger
post *bodyinteger
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/conversations/1/posts" \
  -H "Content-Type: application/json" \
  -d '{"post":12}'

DELETE/api/v1/conversations/{number}/posts/{post}

Unlink a conversation from a post. Needs a read and write key.

NameInType
numberpathinteger
postpathinteger
curl -X DELETE -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/conversations/1/posts/{post}"

POST/api/v1/conversations/{number}/suggested-reply

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. Needs a read and write key.

NameInType
numberpathinteger
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/conversations/1/suggested-reply"

POST/api/v1/conversations/{number}/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. Needs a read and write key.

NameInType
numberpathinteger
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/conversations/1/summary"

GET/api/v1/support

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).

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/support"

PATCH/api/v1/support

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. Needs a read and write key.

NameInType
inboxbodyboolean
emailbodyboolean
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/support" \
  -H "Content-Type: application/json" \
  -d '{"inbox":true}'

POST/api/v1/support/email-domain

Send replies by email from your own address (`[email protected]`): 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. Needs a read and write key.

NameInType
address *bodystring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/support/email-domain" \
  -H "Content-Type: application/json" \
  -d '{"address":"[email protected]"}'

POST/api/v1/support/email-domain/check

Ask Amazon SES now whether your domain's DKIM records are in place (it checks again daily on its own). Needs a read and write key.

curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/support/email-domain/check"

DELETE/api/v1/support/email-domain

Stop sending from your own domain: replies come from the project's support address again, and Amazon SES forgets the domain. Needs a read and write key.

curl -X DELETE -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/support/email-domain"

GET/api/v1/boards

List the project's boards.

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/boards"

GET/api/v1/roadmap

The public roadmap: planned, in progress and completed posts.

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/roadmap"

GET/api/v1/changelog

List changelog entries, newest first: published ones, or the team's drafts.

NameInType
statusquerypublished | draft
limitqueryinteger (1–50)
cursorqueryinteger
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/changelog?status=published&limit=10"

GET/api/v1/changelog/{number}

Get a changelog entry by its number (drafts only for the team).

NameInType
numberpathinteger
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/changelog/1"

POST/api/v1/changelog/{number}/reactions

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. Needs a read and write key.

NameInType
numberpathinteger
reaction *bodythumbs_up | heart | tada
reacted *bodyboolean
authorbodyobject
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/changelog/1/reactions" \
  -H "Content-Type: application/json" \
  -d '{"reaction":"tada","reacted":true,"author":{"id":"user-42"}}'

POST/api/v1/changelog/seen

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. Needs a read and write key.

NameInType
atbodystring
authorbodyobject
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/changelog/seen" \
  -H "Content-Type: application/json" \
  -d '{"at":"2026-10-20T09:00:00Z","author":{"id":"user-42"}}'

POST/api/v1/changelog

Write a changelog entry (a draft until it's published), pick the posts it completes and its cover image (an upload's id). Needs a read and write key.

NameInType
title *bodystring
bodybodystring
contentbodydocument
tagsbodyarray
postsbodyarray
coverbodydocument
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/changelog" \
  -H "Content-Type: application/json" \
  -d '{"title":"Dark mode","body":"Switch themes from your profile menu.","tags":["new"],"posts":[12]}'

PATCH/api/v1/changelog/{number}

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). Needs a read and write key.

NameInType
numberpathinteger
titlebodystring
bodybodystring
contentbodydocument
tagsbodyarray
postsbodyarray
coverbodydocument
expectedUpdatedAtbodystring
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/changelog/1" \
  -H "Content-Type: application/json" \
  -d '{"tags":["new","improved"]}'

POST/api/v1/changelog/{number}/draft

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). Needs a read and write key.

NameInType
numberpathinteger
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/changelog/1/draft"

POST/api/v1/changelog/{number}/rewrite

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`. Needs a read and write key.

NameInType
numberpathinteger
mode *bodyshorten | plain | section
sectionbodydocument
instructionbodystring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/changelog/1/rewrite" \
  -H "Content-Type: application/json" \
  -d '{"mode":"section","section":"Dark mode","instruction":"Mention it follows the system setting"}'

POST/api/v1/changelog/{number}/publish

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. Needs a read and write key.

NameInType
numberpathinteger
announcebodyboolean
emailSubscribersbodyboolean
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/changelog/1/publish" \
  -H "Content-Type: application/json" \
  -d '{"announce":true,"emailSubscribers":true}'

POST/api/v1/changelog/{number}/unpublish

Take a changelog entry off the portal (back to a draft); posts keep their status. Needs a read and write key.

NameInType
numberpathinteger
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/changelog/1/unpublish"

DELETE/api/v1/changelog/{number}

Delete a changelog entry. Needs a read and write key.

NameInType
numberpathinteger
curl -X DELETE -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/changelog/1"

GET/api/v1/changelog-style

Get the changelog's writing style: the tone notes and example entries AI drafts follow.

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/changelog-style"

PATCH/api/v1/changelog-style

Set the changelog's writing style for AI drafts: tone notes, and up to 3 published entries (by number) as examples (replaces the list). Needs a read and write key.

NameInType
tonebodystring
examplesbodyarray
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/changelog-style" \
  -H "Content-Type: application/json" \
  -d '{"tone":"Friendly, short sentences, no jargon. Address the reader as you.","examples":[3]}'

GET/api/v1/articles

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).

NameInType
statusquerypublished | draft | deleted
limitqueryinteger (1–100)
cursorqueryinteger
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/articles?status=published&limit=20"

GET/api/v1/articles/{slug}

Get a docs article by its slug (drafts, hidden and AI-only articles only for the team). The team also gets its unpublished `changes`.

NameInType
slugpathstring
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/articles/{slug}"

GET/api/v1/search/articles

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.

NameInType
q *querystring
limitqueryinteger (1–20)
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/search/articles?q=invite+my+team&limit=5"

POST/api/v1/articles/{slug}/feedback

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. Needs a read and write key.

NameInType
slugpathstring
helpful *bodyboolean
commentbodystring
authorbodyobject
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/articles/{slug}/feedback" \
  -H "Content-Type: application/json" \
  -d '{"helpful":false,"comment":"It doesn't say how to invite people by email.","author":{"id":"user-42"}}'

POST/api/v1/docs/ask

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.

NameInType
q *bodystring
followUpTobodystring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/docs/ask" \
  -H "Content-Type: application/json" \
  -d '{"q":"And on mobile?","followUpTo":"cm3q5x0a1000008l4h2gk7v9e"}'

POST/api/v1/docs/escalate

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). Needs a read and write key.

NameInType
questionId *bodystring
email *bodystring
notebodystring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/docs/escalate" \
  -H "Content-Type: application/json" \
  -d '{"questionId":"cm3q5x0a1000008l4h2gk7v9e","email":"[email protected]","note":"I need this for an audit on Friday."}'

GET/api/v1/assistant

Get how the docs' AI answers: tone, answer length, standing instructions and off-limits topics.

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/assistant"

PATCH/api/v1/assistant

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. Needs a read and write key.

NameInType
tonebodystring
lengthbodyshort | medium | detailed
instructionsbodystring
offLimitsbodyarray
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/assistant" \
  -H "Content-Type: application/json" \
  -d '{"tone":"Friendly and plain; address the reader as you","length":"short","instructions":"Mention the Team page when a question is about inviting people.","offLimits":["refund","legal advice"]}'

GET/api/v1/docs/insights

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).

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/docs/insights"

POST/api/v1/docs/gaps/dismiss

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). Needs a read and write key.

NameInType
question *bodystring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/docs/gaps/dismiss" \
  -H "Content-Type: application/json" \
  -d '{"question":"Can I pay with crypto?"}'

GET/api/v1/reports

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).

NameInType
rangequerytoday | yesterday | month | last-month | 7d | 14d | 30d | quarter | year
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/reports?range=7d"

POST/api/v1/articles

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. Needs a read and write key.

NameInType
title *bodystring
bodybodystring
contentbodydocument
collectionbodydocument
showUpdatedDatebodyboolean
listingbodyeverywhere | unlisted | hidden | ai_only
descriptionbodystring
keywordsbodyarray
noindexbodyboolean
accessbodyinherit | signed_in | groups
groupsbodyarray
noAccessbodyinherit | lock | hide
slugbodystring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/articles" \
  -H "Content-Type: application/json" \
  -d '{"title":"Getting started","body":"Create a board, then share your portal.","collection":"basics"}'

PATCH/api/v1/articles/{slug}

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). Needs a read and write key.

NameInType
slugpathstring
titlebodystring
bodybodystring
contentbodydocument
collectionbodydocument
showUpdatedDatebodyboolean
listingbodyeverywhere | unlisted | hidden | ai_only
descriptionbodystring
keywordsbodyarray
noindexbodyboolean
accessbodyinherit | signed_in | groups
groupsbodyarray
noAccessbodyinherit | lock | hide
newSlugbodydocument
publishbodyboolean
revisionbodystring
seenbodystring
ownbodystring
editedbodyarray
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/articles/{slug}" \
  -H "Content-Type: application/json" \
  -d '{"body":"Owners and admins invite from the Team page.","publish":true,"revision":"cm3x9a1b2c3d4e5f6g7h8i9j0"}'

POST/api/v1/articles/{slug}/publish

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). Needs a read and write key.

NameInType
slugpathstring
editsbodyarray
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/articles/{slug}/publish"

POST/api/v1/articles/{slug}/publish-changes

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). Needs a read and write key.

NameInType
slugpathstring
savedAtbodystring
revisionbodystring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/articles/{slug}/publish-changes"

POST/api/v1/articles/{slug}/discard-changes

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. Needs a read and write key.

NameInType
slugpathstring
savedAtbodystring
revisionbodystring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/articles/{slug}/discard-changes"

POST/api/v1/articles/{slug}/unpublish

Take a docs article off the portal (back to a draft, with its unpublished changes as the draft's text). Needs a read and write key.

NameInType
slugpathstring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/articles/{slug}/unpublish"

DELETE/api/v1/articles/{slug}

Delete a docs article; the team can restore it (with articles.restore) for 30 days. Needs a read and write key.

NameInType
slugpathstring
curl -X DELETE -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/articles/{slug}"

GET/api/v1/articles/{slug}/revisions

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).

NameInType
slugpathstring
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/articles/{slug}/revisions"

POST/api/v1/articles/{slug}/restore

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. Needs a read and write key.

NameInType
slugpathstring
revisionbodystring
dismissEditsbodyarray
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/articles/{slug}/restore"

POST/api/v1/articles/{slug}/verify

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. Needs a read and write key.

NameInType
slugpathstring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/articles/{slug}/verify"

GET/api/v1/articles/{slug}/verifications

A docs article's verification history (articles.verify), newest first: each run's commit, what it found per statement, and its fix.

NameInType
slugpathstring
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/articles/{slug}/verifications"

POST/api/v1/docs/verify-all

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. Needs a read and write key.

curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/docs/verify-all"

POST/api/v1/imports

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). 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. Needs a read and write key.

NameInType
source *bodystring
key *bodystring
parts *bodyarray
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/imports"

GET/api/v1/imports/latest

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).

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/imports/latest"

POST/api/v1/articles/{slug}/reorder

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. Needs a read and write key.

NameInType
slugpathstring
collectionbodydocument
positionbodyinteger (0–10000)
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/articles/{slug}/reorder" \
  -H "Content-Type: application/json" \
  -d '{"collection":"basics","position":0}'

GET/api/v1/collections

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.

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/collections"

POST/api/v1/collections

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`). Needs a read and write key.

NameInType
name *bodystring
descriptionbodystring
iconbodystring,null
accessbodyinherit | signed_in | groups
groupsbodyarray
noAccessbodyinherit | lock | hide
parentbodydocument
tabbodystring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/collections" \
  -H "Content-Type: application/json" \
  -d '{"name":"Getting started","description":"The basics, in five minutes.","icon":"rocket"}'

PATCH/api/v1/collections/{slug}

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. Needs a read and write key.

NameInType
slugpathstring
namebodystring
descriptionbodystring
accessbodyinherit | signed_in | groups
groupsbodyarray
noAccessbodyinherit | lock | hide
iconbodystring,null
parentbodydocument
tabbodystring
orderbodyinteger (0–10000)
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/collections/{slug}" \
  -H "Content-Type: application/json" \
  -d '{"name":"Basics","parent":null}'

POST/api/v1/collections/{slug}/reorder

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. Needs a read and write key.

NameInType
slugpathstring
parentbodydocument
tabbodystring
positionbodyinteger (0–10000)
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/collections/{slug}/reorder" \
  -H "Content-Type: application/json" \
  -d '{"parent":null,"position":0}'

DELETE/api/v1/collections/{slug}

Delete a docs collection; the collections in it move up a level, in its place, and its articles stay, without a collection. Needs a read and write key.

NameInType
slugpathstring
curl -X DELETE -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/collections/{slug}"

GET/api/v1/tabs

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.

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/tabs"

POST/api/v1/tabs

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`). Needs a read and write key.

NameInType
name *bodystring
accessbodyinherit | signed_in | groups
groupsbodyarray
noAccessbodyinherit | lock | hide
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/tabs" \
  -H "Content-Type: application/json" \
  -d '{"name":"Guides"}'

PATCH/api/v1/tabs/{slug}

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. Needs a read and write key.

NameInType
slugpathstring
namebodystring
landingbodyoverview | first | page
landingPagebodystring
accessbodyinherit | signed_in | groups
groupsbodyarray
noAccessbodyinherit | lock | hide
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/tabs/{slug}" \
  -H "Content-Type: application/json" \
  -d '{"name":"API reference","landing":"page","landingPage":"getting-started"}'

POST/api/v1/tabs/{slug}/reorder

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. Needs a read and write key.

NameInType
slugpathstring
positionbodyinteger (0–10000)
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/tabs/{slug}/reorder" \
  -H "Content-Type: application/json" \
  -d '{"position":0}'

DELETE/api/v1/tabs/{slug}

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. Needs a read and write key.

NameInType
slugpathstring
curl -X DELETE -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/tabs/{slug}"

GET/api/v1/groups

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.*).

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/groups"

POST/api/v1/groups

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" }`. Needs a read and write key.

NameInType
name *bodystring
descriptionbodystring
conditions *bodyarray
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/groups" \
  -H "Content-Type: application/json" \
  -d '{"name":"Pro customers","conditions":[{"property":"plan","op":"is_any_of","value":["pro","enterprise"]},{"property":"support_tier","op":"is_set"}]}'

PATCH/api/v1/groups/{group}

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. Needs a read and write key.

NameInType
grouppathstring
namebodystring
descriptionbodystring
conditionsbodyarray
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/groups/{group}" \
  -H "Content-Type: application/json" \
  -d '{"name":"Enterprise customers","conditions":[{"property":"plan","op":"is","value":"enterprise"}]}'

DELETE/api/v1/groups/{group}

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. Needs a read and write key.

NameInType
grouppathstring
curl -X DELETE -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/groups/{group}"

GET/api/v1/users

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.

NameInType
qquerystring
groupquerystring
companyquerystring
sortqueryname | posts | votes | conversations | lastSeen
limitqueryinteger (1–100)
cursorqueryinteger
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/users?q=acme.com&group=enterprise&limit=20"

GET/api/v1/companies

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.

NameInType
sortqueryusers | votes | total
limitqueryinteger (1–100)
cursorqueryinteger
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/companies?sort=total&limit=20"

GET/api/v1/users/{user}

One of the project's users, by their id in your app: name, email, properties and the reader groups they're in now.

NameInType
userpathstring
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/users/{user}"

POST/api/v1/users

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. Needs a read and write key.

NameInType
users *bodyarray
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/users" \
  -H "Content-Type: application/json" \
  -d '{"users":[{"id":"user-42","name":"Sam from Acme","email":"[email protected]","properties":{"plan":"enterprise","seats":40,"features":["sso"],"trial":null}}]}'

GET/api/v1/posts

List posts, most voted or newest first; merged duplicates are left out.

NameInType
boardquerystring
statusqueryopen | under_review | planned | in_progress | completed | closed
sortquerytop | new
limitqueryinteger (1–100)
cursorqueryinteger
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/posts?status=planned&sort=top&limit=10"

GET/api/v1/posts/{number}

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`).

NameInType
numberpathinteger
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/posts/1"

GET/api/v1/posts/{number}/comments

List a post's comments, oldest first: to the team (an API key, a team member) with its internal notes (`internal: true`).

NameInType
numberpathinteger
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/posts/1/comments"

POST/api/v1/posts

Create a post, as the key's creator or on behalf of someone from your app (`author`). Needs a read and write key.

NameInType
board *bodystring
title *bodystring
bodybodystring
contentbodydocument
authorbodyobject
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/posts" \
  -H "Content-Type: application/json" \
  -d '{"board":"feature-requests","title":"Dark mode","body":"The dashboard is too bright at night.","author":{"id":"user-42","name":"Sam","email":"[email protected]"}}'

POST/api/v1/posts/{number}/comments

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`). Needs a read and write key.

NameInType
numberpathinteger
bodybodystring
contentbodydocument
authorbodyobject
internalbodyboolean
emailVotersbodyboolean
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/posts/1/comments" \
  -H "Content-Type: application/json" \
  -d '{"body":"Thanks! This is planned for next month."}'

PATCH/api/v1/posts/{number}

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). Needs a read and write key.

NameInType
numberpathinteger
statusbodyopen | under_review | planned | in_progress | completed | closed
boardbodystring
ownerbodydocument
etabodydocument
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/posts/1" \
  -H "Content-Type: application/json" \
  -d '{"status":"planned"}'

POST/api/v1/posts/{number}/merge

Merge a duplicate into another post: votes and comments move over, and the duplicate leaves every list. Needs a read and write key.

NameInType
numberpathinteger
into *bodyinteger
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/posts/1/merge" \
  -H "Content-Type: application/json" \
  -d '{"into":12}'

DELETE/api/v1/posts/{number}/links/{id}

Remove a link from a post. Needs a read and write key.

NameInType
numberpathinteger
idpathstring
curl -X DELETE -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/posts/1/links/{id}"

GET/api/v1/code-changes

List code changes (Autopilot): merged pull requests, each with the updates it suggests (those with updates waiting first).

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/code-changes"

GET/api/v1/code-changes/{id}

Get a code change with its proposed actions.

NameInType
idpathstring
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/code-changes/{id}"

GET/api/v1/code-change-targets

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.

NameInType
viewquerywaiting | decided
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/code-change-targets?view=waiting"

GET/api/v1/code-change-targets/{type}/{id}

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).

NameInType
typepathfeedback | articles
idpathstring
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/code-change-targets/{type}/{id}"

POST/api/v1/code-changes/{id}/analyse

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. Needs a read and write key.

NameInType
idpathstring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/code-changes/{id}/analyse"

POST/api/v1/code-changes/resume

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. Needs a read and write key.

curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/code-changes/resume"

POST/api/v1/code-change-actions/{id}/apply

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). Needs a read and write key.

NameInType
idpathstring
commentbodystring
linebodystring
revisionbodystring
reviewedAsbodypublished | draft
collectionbodydocument
publishbodydocument
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/code-change-actions/{id}/apply" \
  -H "Content-Type: application/json" \
  -d '{"comment":"Weekly repeats are out today; monthly ones are next."}'

POST/api/v1/code-change-actions/apply

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. Needs a read and write key.

NameInType
actions *bodyarray
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/code-change-actions/apply" \
  -H "Content-Type: application/json" \
  -d '{"actions":[{"id":"cm3x9a1b2c3d4e5f6g7h8i9j0"},{"id":"cm3x9a1b2c3d4e5f6g7h8i9j1","comment":"Weekly repeats are out today."}]}'

POST/api/v1/code-change-actions/{id}/undo

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. Needs a read and write key.

NameInType
idpathstring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/code-change-actions/{id}/undo"

POST/api/v1/code-change-actions/{id}/publish-now

Publish now: a scheduled action (going out by itself after the hold) is applied at once, as codeChangeActions.apply does, by whoever asks. Needs a read and write key.

NameInType
idpathstring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/code-change-actions/{id}/publish-now"

POST/api/v1/code-change-actions/{id}/stop

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). Needs a read and write key.

NameInType
idpathstring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/code-change-actions/{id}/stop"

POST/api/v1/code-change-actions/{id}/dismiss

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. Needs a read and write key.

NameInType
idpathstring
reasonbodystring
revisionbodystring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/code-change-actions/{id}/dismiss" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Only part of it; we'll say so when it's all out"}'

POST/api/v1/posts/{number}/vote

Vote for a post or take the vote back, as the key's creator or on behalf of someone from your app. Needs a read and write key.

NameInType
numberpathinteger
voted *bodyboolean
authorbodyobject
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/posts/1/vote" \
  -H "Content-Type: application/json" \
  -d '{"voted":true,"author":{"id":"user-42"}}'

POST/api/v1/posts/{number}/subscription

Follow a post without voting for it, or stop: its followers are emailed when it ships, as its voters are. Needs a read and write key.

NameInType
numberpathinteger
subscribed *bodyboolean
authorbodyobject
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/posts/1/subscription" \
  -H "Content-Type: application/json" \
  -d '{"subscribed":true,"author":{"id":"user-42"}}'

PATCH/api/v1/email-preferences

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). Needs a read and write key.

NameInType
ownPostsbodyboolean
followedPostsbodyboolean
unsubscribedbodyboolean
changelogbodyboolean
authorbodyobject
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/email-preferences" \
  -H "Content-Type: application/json" \
  -d '{"followedPosts":false,"author":{"id":"user-42"}}'

GET/api/v1/duplicates

List possible duplicates waiting for the team: a newer post that may repeat an older one.

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/duplicates"

POST/api/v1/duplicates/{id}/dismiss

Mark a possible duplicate as not a duplicate (it won't be suggested again). Needs a read and write key.

NameInType
idpathstring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/duplicates/{id}/dismiss"

GET/api/v1/widget

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).

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/widget"

PATCH/api/v1/widget

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). Needs a read and write key.

NameInType
labelbodystring
colorbodydocument
positionbodybottom-right | bottom-left
linksbodyarray
iconbodychat | help | megaphone | lightbulb | image
iconImagebodydocument
titlebodystring
greetingbodystring
hiddenbodyarray
onPortalbodyboolean
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/widget" \
  -H "Content-Type: application/json" \
  -d '{"label":"Help & feedback","color":"#5b21b6","position":"bottom-left","links":[{"label":"Status","url":"https://status.example.com"}],"icon":"help","title":"Questions about Acme?","hidden":["whatsNew"]}'

GET/api/v1/branding

Get the portal's branding: logos, favicon, colour, header and footer links, and whether it says "Powered by tuti".

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/branding"

PATCH/api/v1/branding

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. Needs a read and write key.

NameInType
logobodydocument
logoDarkbodydocument
faviconbodydocument
colorbodydocument
headerLinksbodyarray
footerLinksbodyarray
poweredBybodyboolean
contentActionsbodyobject
docsHomebodyobject
colorSchemebodysystem | light | dark
logoTextbodystring
logoLinkbodydocument
sidebarLinksbodyarray
seobodyobject
seoImagebodydocument
fontsbodyobject
customCssbodystring
customHtmlbodystring
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/branding" \
  -H "Content-Type: application/json" \
  -d '{"color":"#5b21b6","headerLinks":[{"label":"Docs","url":"https://docs.example.com"}],"footerLinks":[{"label":"Privacy","url":"https://example.com/privacy"}],"poweredBy":false}'

POST/api/v1/license/check

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. Needs a read and write key.

curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/license/check"

GET/api/v1/usage

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.

NameInType
monthsqueryinteger (1–24)
curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/usage?months=3"

GET/api/v1/ai-models

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.

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/ai-models"

PATCH/api/v1/ai-models

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. Needs a read and write key.

NameInType
answersbodystring
escalationsbodystring
changelogbodystring
code_changesbodystring
supportbodystring
code_changes_decidebodystring
code_changes_redecidebodystring
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/ai-models" \
  -H "Content-Type: application/json" \
  -d '{"answers":"anthropic/claude-sonnet-5.5","changelog":""}'

GET/api/v1/code-changes-tuning

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.

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/code-changes-tuning"

PATCH/api/v1/code-changes-tuning

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. Needs a read and write key.

NameInType
propose.edit.maybebodydocument
propose.newArticle.maybebodydocument
redecide.whenNewArticlebodydocument
decide.roundsbodydocument
write.roundsbodydocument
decide.reasoningbodydocument
context.diffCharsbodydocument
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/code-changes-tuning" \
  -H "Content-Type: application/json" \
  -d '{"propose.edit.maybe":0.4,"decide.rounds":8,"decide.reasoning":null}'

GET/api/v1/credits

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).

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/credits"

POST/api/v1/credits/top-up

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. Needs a read and write key.

NameInType
dollars *bodyinteger (5–1000)
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/credits/top-up" \
  -H "Content-Type: application/json" \
  -d '{"dollars":25}'

GET/api/v1/plan

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).

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/plan"

POST/api/v1/plan/subscribe

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. Needs a read and write key.

NameInType
plan *bodypro | business
intervalbodymonth | year
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/plan/subscribe" \
  -H "Content-Type: application/json" \
  -d '{"plan":"pro","interval":"month"}'

POST/api/v1/plan/manage

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. Needs a read and write key.

curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/plan/manage"

POST/api/v1/plan/badge

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 }`). Needs a read and write key.

NameInType
on *bodyboolean
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/plan/badge" \
  -H "Content-Type: application/json" \
  -d '{"on":true}'

GET/api/v1/portal

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.

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/portal"

PATCH/api/v1/portal

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). Needs a read and write key.

NameInType
accessbodypublic | private
homebodyfeedback | roadmap | changelog | docs
curl -X PATCH -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/portal" \
  -H "Content-Type: application/json" \
  -d '{"access":"private","home":"docs"}'

GET/api/v1/portal/readers

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.

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/portal/readers"

POST/api/v1/portal/readers

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. Needs a read and write key.

NameInType
emails *bodyarray
notifybodyboolean
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/portal/readers" \
  -H "Content-Type: application/json" \
  -d '{"emails":["[email protected]"]}'

DELETE/api/v1/portal/readers/{email}

Stop letting someone (an address) or a domain ("@acme.com") read the private portal (the team and SSO users keep their access); accounts stay. Needs a read and write key.

NameInType
emailpathstring
curl -X DELETE -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/portal/readers/{email}"

GET/api/v1/domain

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).

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/domain"

POST/api/v1/domain

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). Needs a read and write key.

NameInType
hostname *bodystring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/domain" \
  -H "Content-Type: application/json" \
  -d '{"hostname":"help.example.com"}'

POST/api/v1/domain/verify

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. Needs a read and write key.

curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/domain/verify"

DELETE/api/v1/domain

Stop using the custom domain: the portal answers only at its Tuti address again. Needs a read and write key.

curl -X DELETE -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/domain"

GET/api/v1/site

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.

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/site"

POST/api/v1/site

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). Needs a read and write key.

NameInType
url *bodystring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/site" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/portal"}'

POST/api/v1/site/verify

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. Needs a read and write key.

curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/site/verify"

DELETE/api/v1/site

Stop putting the portal under your site's path: it answers at its own address again. Needs a read and write key.

curl -X DELETE -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/site"

GET/api/v1/redirects

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).

curl -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/redirects"

POST/api/v1/redirects

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. Needs a read and write key.

NameInType
from *bodystring
to *bodystring
curl -X POST -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/redirects" \
  -H "Content-Type: application/json" \
  -d '{"from":"/hc/en-us/articles/123-billing","to":"/docs/billing"}'

DELETE/api/v1/redirects/{id}

Stop a redirect (`id`, from redirects.list): its old address is the portal's again. Needs a read and write key.

NameInType
idpathstring
curl -X DELETE -H "Authorization: Bearer $TUTI_API_KEY" "https://tuti.so/api/v1/redirects/{id}"