Документация для разработчиков
Последнее обновление 26 сентября 2026.
Всё, что Changeloop публикует для вас, — это простой JSON по HTTPS. Не нужно устанавливать SDK, ротировать API-ключ или проходить вход: два фида ниже — это анонимные публичные чтения, ключом к которым служит ID вашего фида. Замените YOUR_PUBLIC_ID на свой в любом примере на этой странице.
Одна вещь, которую нужно знать перед началом: ID вашего публичного фида находится прямо в приложении. Войдите, откройте Настройки, и он там, в разделе Публичный фид, том, в который вы попадаете по умолчанию, вместе с готовыми ссылками на changelog.json и roadmap.json, ссылкой на вашу размещённую страницу фида и фрагментом кода виджета ниже, каждый со своей кнопкой копирования.
С чего начать
Пять шагов ведут от регистрации к changelog на вашем собственном сайте. Страница «Get started» в приложении проведёт вас по ним и отметит каждый шаг, как только он будет выполнен.
- Подключите источник: репозиторий GitHub, проект GitLab или репозиторий Bitbucket.
- Выберите язык, на котором пишутся ваши записи.
- При желании создайте теги, чтобы читатели могли фильтровать записи по частям продукта.
- Опубликуйте первую запись. Слитые изменения попадают в папку проверки в виде черновиков: одобрите один или включите автопубликацию для этого репозитория.
- Разместите его на сайте: дайте ссылку на размещённую у нас страницу, вставьте виджет или выведите JSON-фид на своей странице.
Ваш changelog примерно в десяти строках React
Вставьте это в компонент, и у вас есть работающий changelog. Больше добавлять нечего.
import { useEffect, useState } from 'react';
const FEED = 'https://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/changelog.json';
export function Changelog() {
const [entries, setEntries] = useState([]);
useEffect(() => {
fetch(FEED).then((r) => r.json()).then((feed) => setEntries(feed.data));
}, []);
return <ul>{entries.map((e) => <li key={e.id}><b>{e.title}</b><p>{e.mdContent}</p></li>)}</ul>;
}
mdContent — это markdown, который мы составили, в виде текста. Если вы предпочитаете рендерить форматированный вывод, используйте вместо этого htmlContent: он строится на сервере нашим собственным санитайзером из фиксированного списка разрешённых тегов и атрибутов, и это единственное значение в любом из этих ответов, предназначенное для вставки как разметка. Всё остальное — текст, а записи, составленные из публичного репозитория, могут быть подвержены влиянию любого, кто может открыть там pull request, так что относитесь к ним соответственно.
Фид changelog
GET/v1/public/YOUR_PUBLIC_ID/changelog.jsonВаши опубликованные записи, от самых новых, при этом самый новый ID разрешает ничьи при одинаковых временных метках.
Параметры запроса
- repos принимает список полных имён репозиториев через запятую, например acme/web,acme/api. Возвращаются только записи из этих репозиториев. Оставьте пустым, и вы получите все.
- limit — это сколько записей вы хотите на страницу. По умолчанию 20, всё выше 50 ограничивается до 50, а всё, что мы не можем интерпретировать как положительное число, возвращается к 20 вместо ошибки.
- cursor непрозрачен. Возьмите значение nextCursor из предыдущего ответа и верните его как есть. Курсор, который мы не можем декодировать, обрабатывается так, будто курсора нет, поэтому вы снова получаете первую страницу вместо ошибки.
Ответ
{
"data": [
{
"id": "66b0c1f2e4a9d1c3b5a70011",
"title": "Saved views on the inbox",
"mdContent": "You can now pin a filter and come back to it.",
"htmlContent": "<p>You can now pin a filter and come back to it.</p>",
"repoFullName": "acme/web",
"category": "feature",
"tags": ["Inbox"],
"learnMoreUrl": "https://acme.example/docs/saved-views",
"publishedAt": "2026-08-06T09:12:44.000Z"
}
],
"nextCursor": null,
"tagColors": { "Inbox": "#4f46e5" }
}
Каждая запись несёт одни и те же девять ключей: id, title, mdContent, htmlContent, repoFullName, category, tags, learnMoreUrl и publishedAt. category — это feature, fix или internal, и равно null, если составитель его не установил; publishedAt — строка ISO 8601, а htmlContent — пустая строка для записи, никогда не проходившей через составитель. tags — массив названий ваших собственных областей продукта и пуст, если ни одна не назначена, learnMoreUrl равно null, если только рецензент не добавил его, а цвет для рисования каждого тега берётся из карты tagColors в ответе, а не из записи, поэтому тег, который вы удалили из своего словаря, просто рендерится без цвета. nextCursor равно null, когда вы достигли конца.
Неизвестный ID фида отвечает 404 с {"error":"not_found"}, как и некорректно сформированный. Эти два случая намеренно неразличимы, поэтому этот endpoint нельзя использовать, чтобы выяснить, какие ID существуют.
Фид roadmap
GET/v1/public/YOUR_PUBLIC_ID/roadmap.jsonТе же три колонки, которые ваша команда уже ведёт вручную.
{
"columns": [
{ "column": "planned", "items": [], "hasMore": false },
{
"column": "building",
"items": [
{
"id": "66b0c1f2e4a9d1c3b5a70042",
"column": "building",
"publicTitle": "Slack notifications",
"publicDescription": "Post each published entry to a channel you pick.",
"publishedAt": "2026-08-05T16:20:01.000Z"
}
],
"hasMore": false
},
{ "column": "shipped", "items": [], "hasMore": false }
]
}
columns — это массив, а не объект, индексированный по имени колонки, и его порядок является частью контракта: planned, затем building, затем shipped. Все три всегда присутствуют, включая пустые, поэтому вам никогда не нужно отличать «такой колонки нет» от «в ней ещё ничего нет». Рендерите в том порядке, в котором получили, и вы будете соответствовать любой другой создаваемой нами поверхности.
Элемент имеет ровно пять ключей: id, column, publicTitle, publicDescription и publishedAt. publicDescription всегда строка и может быть пустой, никогда не null. Ничего об issue, из которого пришёл элемент, здесь не раскрывается — ни репозиторий, ни номер issue, и это сделано намеренно, а не упущение, которое мы восполним позже.
Этот endpoint вообще не принимает параметров запроса. Нет ни курсора, ни лимита, ни фильтра репозитория, потому что roadmap — это маленькая доска, которую курирует человек, а не журнал, растущий вечно. Каждая колонка возвращает до 50 элементов и устанавливает hasMore, если их было больше. hasMore носит информативный характер: нет курсора, чтобы следовать за ним, так что не стройте вокруг этого пагинацию.
publicTitle и publicDescription — это обычный текст, составленный из заголовков и тел issue, на которые в публичном репозитории может повлиять любой, кто откроет issue. Они не несут никакой гарантии санитизации HTML и не являются исключением htmlContent. Рендерите их как текст.
Встраиваемый виджет
Если вы предпочитаете ничего не строить, вставьте эти две строки. Виджет — это custom element, рендерящийся в shadow root, поэтому он не наследует ваши стили и не влияет на них.
<script src="https://api.changeloop.dev/widget.js" defer></script>
<changelogapp-widget
data-public-id="YOUR_PUBLIC_ID"
data-api="https://api.changeloop.dev"></changelogapp-widget>
Оба атрибута обязательны. data-public-id — это ID вашего фида, data-api — источник, с которого виджет получает данные. Если какого-то из них не хватает, элемент пишет ошибку в консоль и ничего не рендерит, что является первым, что нужно проверить, если вы видите пустое место там, где должен быть виджет.
Добавьте элементу data-theme="dark" для тёмного отображения; ваша страница может переключать его на лету. Для более тонкой настройки виджет предоставляет CSS-переменные (--changelogapp-text, --changelogapp-bg, --changelogapp-accent и другие) и имена ::part(), которые вы задаёте в собственной таблице стилей. Приложение показывает обе темы вживую в Настройках, раздел Публичный фид.
Добавьте data-repos, чтобы показывать только часть репозиториев, например changelog одного продукта на сайте этого продукта, когда несколько продуктов используют один аккаунт. Значение - список полных имён owner/repo через запятую; имя без владельца ничему не соответствует и выводит пустую ленту без ошибки. Учитывается не более десяти репозиториев. Ограниченный так виджет показывает только вкладки Updates и Feedback, потому что у роадмапа нет представления по репозиториям, а отзывы по-прежнему попадают туда, куда указывает цель отзывов вашей команды. В разделе Настройки, Публичная лента есть селектор, который запишет атрибут за вас.
Он рендерит три вкладки в этом порядке: Updates, Roadmap и Feedback. Первые две читают фиды выше. Третья отправляет на endpoint ниже и хранит ID каждой отправки в localStorage, поэтому посетитель может вернуться и увидеть, что произошло с тем, что он отправил.
Скрипт подаётся с версионированием. /widget.js всегда подаёт новейшую сборку и кэшируется на час, поэтому релиз доходит до ваших посетителей без каких-либо действий с вашей стороны. /widget-vN.js фиксирует одну сборку: как только номер версии был подан, её байты больше никогда не меняются, и она кэшируется на год. Зафиксируйте её, если предпочитаете принимать изменения намеренно.
Загружайте ровно один скрипт виджета на страницу
Два URL — это альтернативы, а не слои. Оба регистрируют одно и то же имя custom element, а браузер позволяет зарегистрировать имя только один раз на документ: какой скрипт выполнится первым, тот и побеждает на весь срок жизни страницы, а второй становится неактивным. Так что страница, содержащая и /widget.js, и /widget-v5.js, рендерит тот, который браузер выполнил первым, а это не то, что вы контролируете, и добавление /widget-v5.js рядом с существующим /widget.js для фиксации версии ничего не делает.
Когда это происходит, виджет пишет предупреждение в консоль, называя обе сборки, так что вы не остаётесь гадать. Он не может сделать больше, чем предупредить: к моменту выполнения второй копии первая уже заняла имя. Решение — всегда заменять тег скрипта, а не добавлять другой, и то же самое применимо, если менеджер тегов или частичный шаблон вставляет его за вас. Чтобы перейти от постоянно обновляемой сборки к зафиксированной, измените src.
Размещённая страница фида
https://feed.changeloop.dev/feed/YOUR_PUBLIC_IDМы также размещаем простую страницу по этому адресу: ваш changelog и вашу доску roadmap, отрендеренные из тех же двух фидов выше. Не требует входа и ничего настроенного с вашей стороны. Это также место, куда мы возвращаем людей, когда цикл замыкается: комментарий Shipped, который мы оставляем на issue GitHub, ссылается сюда, как и shippedEntry.link из поиска отправки выше, оба приводят к доставленной записи с её собственной якорной ссылкой #entry-ID, которая всё ещё находит запись, даже если она с тех пор переместилась на более позднюю страницу.
Относитесь к этому как к резервному варианту, а не к интеграции. Фид changelog и виджет остаются способом разместить это на вашем собственном сайте, чтобы это выглядело как ваш продукт, а не наш; эта страница для случаев, когда вы этого ещё не сделали, и для ссылок замыкания цикла, которые указывают сюда независимо от того, что ещё вы построили.
Ваш собственный домен
Вы можете отдавать размещённую у нас страницу со своего адреса, без изменений DNS или сертификатов. В Настройках, раздел Свой домен, вставьте публичный адрес, который увидят читатели (например https://example.com/changelog), затем направьте этот путь на вашем сайте на показанную там цель прокси: одно правило покрывает страницу, её ресурсы, данные и фиды. Проверить мой домен запрашивает ваш адрес с нашей стороны и сообщает, правильно ли настроен прокси, а если нет, что изменить.
Сервер MCP
POSThttps://api.changeloop.dev/mcpЕсли вы работаете в Claude Code, ChatGPT или другом агенте, говорящем на Model Context Protocol, вы можете подключить его напрямую к своему changelog. Тогда агент сможет видеть, что ожидает проверки, редактировать текст и публиковать, не выходя из редактора. Это тот же барьер проверки, что и в веб-приложении: ничего не становится публичным, пока что-то не одобрит это.
Подключение Claude Code
Сначала создайте API-ключ (Настройки, API-ключи), затем добавьте сервер со своим ключом в заголовке:
claude mcp add --transport http changeloop \
https://api.changeloop.dev/mcp \
--header "Authorization: Bearer clapi_YOUR_KEY"
Для клиента, который вместо этого читает JSON-конфигурацию, то же самое выглядит так:
{
"mcpServers": {
"changeloop": {
"type": "http",
"url": "https://api.changeloop.dev/mcp",
"headers": { "Authorization": "Bearer clapi_YOUR_KEY" }
}
}
}
Пока нет потока OAuth. Аутентификация — это API-ключ в заголовке, что и делают две команды выше. Отзыв этого ключа в Настройках отключает агента при его следующем запросе.
Что может делать агент
Семь инструментов, и список намеренно короткий. Всё остальное, что может делать этот продукт, доступно через REST API с тем же ключом; каждый инструмент, доступный агенту, — это ещё одна вещь, к вызову которой его можно склонить.
- list_pending_entries, list_published_entries, get_entry - читают ваши записи. Ожидающие не являются публичными.
- update_entry - меняет заголовок или markdown-содержимое записи. HTML, который подаёт фид, повторно рендерится из вашего markdown нашим санитайзером; агент не может предоставить HTML.
- approve_entry - публикует. Это публично и немедленно, и уведомляет любую связанную обратную связь на GitHub. Одобрить можно только ожидающую запись.
- discard_entry - оставляет запись вне changelog. Обратимо из веб-приложения.
- get_changelog_info - ID вашего фида и адреса, по которым подаётся ваш changelog.
Чего он не может
Каждый инструмент ограничен командой, которой принадлежит ключ, и ни один не принимает команду в качестве аргумента, так что нет ничего, что могло бы указать на другую команду, даже если бы что-то попыталось. Сервер не принимает сессию браузера, только ключ: запрос должен намеренно прикрепить учётные данные. И ключ не может управлять ключами или скачивать экспорт ваших данных, так что агент, подключённый таким образом, не может создать себе вторую учётную запись или извлечь ваши данные за один вызов.
API-ключи
Всё вышеперечисленное анонимно и не требует учётных данных. Аутентифицированный API - ваши настройки, ваша папка проверки - это другая поверхность, и он принимает либо вошедшую сессию браузера, либо API-ключ. Ключи предназначены для скриптов и агентов: всего, что должно достучаться до вашего changelog без человека за клавиатурой.
Authorization: Bearer clapi_YOUR_KEYСоздайте его в приложении в разделе Настройки, на вкладке API-ключи. Ключ показывается один раз, в момент создания, и никогда больше: мы храним только его хеш, поэтому нет ни одного экрана, который мог бы показать его вам во второй раз. Если вы его потеряли, отзовите его и создайте другой.
Что может и не может делать ключ
Ключ несёт тот же доступ, что и вход в систему, ограниченный одной командой, в которой он был создан, с двумя намеренными исключениями. Он не может управлять API-ключами и не может скачивать экспорт ваших данных. Оба требуют реального входа, чтобы утёкший ключ не мог создать себе замену, не мог отозвать ключи, которые вы бы использовали, чтобы его заблокировать, и не мог извлечь данные вашей команды за один запрос.
Отзыв
Отзыв вступает в силу при следующем запросе. Отозванный ключ отвечает 401 точно так же, как неизвестный, и продолжает отвечать 401 даже из браузера, который всё ещё держит действующую сессию, потому что запрос с заголовком Authorization никогда молча не повторяется как запрос с cookie. Отозванный ключ остаётся в списке с датой отзыва и датой последнего использования, что как раз то, что вам нужно, когда вы выясняете, куда добрался утёкший ключ.
Тарифы и лимиты
Бесплатный тариф описывает 20 слитых изменений в месяц и ограничивает дневное число просмотренных слияний, разобранных отзывов, подготовленных карточек роадмапа и альтернативных версий 50 каждое; у командного тарифа жёстких лимитов нет. Настройки, раздел Тариф и использование, показывает каждый бюджет так, как его считает сам продукт, с моментом сброса, ещё до того, как что-то будет отклонено. Работа, поступившая сверх лимита, откладывается, а не теряется: запись сверх квоты ждёт во входящих, а отклонённый черновик роадмапа можно повторить, когда окно обновится.
GitLab и Bitbucket
Проект GitLab или репозиторий Bitbucket может питать ваш changelog так же, как это делает репозиторий GitHub: подключите его в Настройках, затем GitLab, или в Настройках, затем Bitbucket, добавьте предоставленный нами webhook (или на bitbucket.org позвольте Connect with Bitbucket добавить его, если на странице Bitbucket есть такая кнопка), и каждое изменение, объединённое в указанную вами ветку, становится черновиком записи в вашей папке проверки, написанной так же и проходящей ту же проверку человеком. Записи создаются из объединённых pull request'ов или merge request'ов, а на GitHub и Bitbucket также из push'ей, если вы выберете режим push в Настройках, затем What creates drafts. Проекты GitLab создают черновики только из merge request'ов.
Подключение проекта
Проекты GitLab подключаются в Настройках, затем GitLab, а репозитории Bitbucket в Настройках, затем Bitbucket. Введите путь (в GitLab группу и проект, например acme/web, в Bitbucket workspace и репозиторий, например acme/app), и мы вернём вам адрес webhook и секрет. Вставьте оба в настройки webhook на их стороне: в GitLab отметьте Merge request events, в Bitbucket отметьте триггеры Merged pull request и Push repository. Самостоятельно размещённые экземпляры работают через https. Секрет показывается один раз, в этот момент. Если вы его потеряли, удалите проект и подключите заново. На bitbucket.org, если на странице Bitbucket отображается кнопка Connect with Bitbucket, можно обойтись без вставки: нажмите её, один раз разрешите доступ, и мы прочитаем основную ветку репозитория и добавим webhook за вас. Для этого нужны права администратора репозитория. Для самостоятельно размещённого Bitbucket или если вы предпочитаете вставлять вручную, выберите Set it up by hand и получите адрес и секрет, как описано выше. Если вы удаляете репозиторий Bitbucket и подключаете его заново, удалите и старый webhook в Bitbucket, в Repository settings, затем Webhooks. После подключения проекта вы можете изменить его ветку и включить автопубликацию в его строке, а если доставка была проигнорирована, строка покажет причину.
Почему Bitbucket спрашивает о ветке, а GitLab нет
GitLab сообщает нам, какую ветку ваш проект считает веткой по умолчанию, так что вы можете оставить поле пустым, и это будет означать именно это. Bitbucket вообще не отправляет ветку по умолчанию, так что если бы мы позволили вам оставить поле пустым, нам не с чем было бы сравнивать, и ваш webhook выглядел бы идеально установленным, никогда не производя ни одной записи. Мы предпочитаем задать один вопрос, чем позволить этому случиться. С Connect with Bitbucket мы запрашиваем у Bitbucket основную ветку, когда вы разрешаете доступ, так что вводить её не нужно.
Что они пока не охватывают
Записи changelog, и ничего больше. Виджет обратной связи, открывающий issue за вас, ответ, размещаемый на этом issue, когда исправление доставлено, публичный roadmap, управляемый метками issue, и предпросмотр исходного кода в папке проверки — всё это сегодня доступно только для GitHub.
Причина в том, что мы предпочитаем сформулировать её прямо, а не скрывать. Каждому из этих решений нужен токен доступа с правом записи в ваш проект, хранимый нами. Записи changelog не нуждаются ни в чём подобном, потому что всё, из чего они пишутся, приходит в самом webhook, так что подключение GitLab или Bitbucket через webhook не даёт нам никаких учётных данных и никакого чтения вашего кода. Connect with Bitbucket является единственным исключением. Bitbucket выдаёт нам на один запрос токен, который может читать репозиторий и его pull request и управлять его webhook, и мы используем его только для чтения основной ветки и добавления webhook, а затем удаляем. Ничего не сохраняется. Мы предпочитаем поставить часть, которая не стоит вам ничего, чем запрашивать токен, чтобы дополнить список функций.
Другие версии записи
Одно изменение обычно нужно объяснить более одного раза: клиентам в changelog, тому, кто отвечает на вопросы о нём, и в канале, где никто не читает четыре абзаца. Из папки проверки вы можете составить одну из двух дополнительных версий записи перед её одобрением.
Версия анонса — это одна-две строки, и именно она публикуется в Slack, когда вы одобряете запись, вместо полного текста. Заметка поддержки — это внутренний брифинг: что изменилось, что заметят клиенты, и предложение, которое агент поддержки мог бы сказать почти дословно. Обе версии — это черновики, которые вы можете переписать до использования, и любую из них можно удалить.
Ни одна из них не публикуется
Эти версии никогда не появляются на вашей странице changelog, в каком-либо фиде, в виджете или в API, который их подаёт. Заметка поддержки, в частности, написана для людей внутри вашей компании и может быть более прямой, чем сама запись. Единственные места, где она существует, — это ваша папка проверки и, если вы их используете, ваша собственная копия.
Из чего они пишутся
Всегда из записи, никогда из pull request'а. Это намеренно: запись уже прошла через правило, которое держит исправления безопасности расплывчатыми, и через вашу собственную проверку. Версия, переписанная из неё, не может повторно ввести деталь, которую вы удалили, потому что этой детали нет в том, что получила модель.
Анонсирование в Slack
Одобрите запись, и она может быть опубликована в канале Slack в тот же момент, когда становится публичной. Подключите это в Настройках, на вкладке Slack: создайте входящий webhook в собственном workspace, выберите канал и вставьте URL. С вашей стороны ничего не устанавливается, кроме этого webhook, и мы не запрашиваем никакого доступа к вашему workspace.
Сообщение несёт заголовок записи, текст в том виде, в каком вы его одобрили, её категорию и теги, и ссылку обратно на запись в вашем changelog. Markdown переводится в то, что Slack действительно рендерит, поэтому запись не приходит с показанными звёздочками.
URL webhook — это учётные данные
Любой, кто владеет этим URL, может публиковать в канал, поэтому мы относимся к нему как к паролю: он хранится, а после этого ни один экран и ни один ответ API больше не показывает его снова, включая ваш собственный экспорт данных. То, что вы видите после, — маска, достаточная, чтобы отличить два webhook друг от друга, и бесполезная для кого-либо ещё. Мы принимаем только адрес hooks.slack.com, так что неправильно введённый или подменённый URL отклоняется, а не запрашивается.
Когда он перестаёт работать
Если вы удалите приложение в Slack или заархивируете канал, webhook навсегда перестаёт работать. Мы замечаем это при первом отклонённом сообщении, отключаем анонсы и указываем это на вкладке Slack с причиной и датой. То, что мы не продолжаем молча повторять попытки, сделано намеренно: changelog, который никто не анонсировал, выглядит точно так же, как тот, который никто не читал, и это разница, о которой стоит сообщить.
Приостановка
Пауза останавливает анонсы и сохраняет webhook, так что возобновление — это одно нажатие, а не ещё один круг через Slack. Отключение полностью удаляет URL. В любом случае сама публикация не затрагивается: Slack — это канал, в который публикует ваш changelog, а не барьер, которого он ждёт. Если Slack недоступен, когда вы что-то одобряете, запись всё равно публикуется, а анонс повторяется сам по себе.
RSS и JSON Feed
GEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/rss.xmlGEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/feed.jsonТе же опубликованные записи в виде фида для подписки, в двух форматах, понятных читателям: RSS 2.0 и JSON Feed 1.1. Оба принимают те же фильтры repos, category и tag, что и фид changelog, и несут тот же Cache-Control и ETag. Ни один не пагинируется: читатель опрашивает начало фида, поэтому эти возвращают только самые последние записи, без курсора.
Текст записи — это санитизированный HTML, обёрнутый в CDATA для RSS и как content_html для JSON Feed. JSON Feed дополнительно несёт цвета ваших тегов под расширением с пространством имён _changelogapp; RSS — нет, потому что ни один читатель их бы не раскрасил.
Размещённая страница анонсирует оба как ссылки rel="alternate", так что браузер или читатель, попавший туда, может подписаться, не зная путей.
Одна запись сама по себе
GEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/entries/ENTRY_IDВозвращает одну опубликованную запись, тот же объект, который фид changelog несёт в своём массиве data. Именно сюда ведут постоянные ссылки в фидах, и это полезно, когда у вас есть ID и вы не хотите пролистывать фид, чтобы найти его. Неизвестный ID, или принадлежащий неопубликованной записи, возвращает 404 с тем же телом, что и любой другой неизвестный ID.
Фид markdown
GEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/changelog.mdТе же опубликованные записи в виде простого markdown, подаваемого как text/markdown. Существует для читателей, не являющихся браузерами: LLM или агент, отвечающий на «что недавно изменилось в этом продукте», получает текст без анализа RSS или обхода JSON. Принимает те же фильтры repos, category и tag, что и фид changelog, несёт тот же Cache-Control и ETag, и отвечает 304 на условный запрос точно так же, как два других.
Каждая запись — это секция: заголовок как заглавие, затем одна строка с датой, категорией и любыми тегами, затем текст записи в том виде, в каком он был написан, затем ссылка Learn more, если у записи она есть, затем её постоянная ссылка. Документ открывается заголовком и описанием вашего фида и ведёт обратно к размещённой странице. Когда ещё ничего не опубликовано, он говорит об этом одним предложением, а не возвращает пустое тело, так что читатель может отличить это от неудачного получения.
Размещённая страница анонсирует его как ссылку rel="alternate" с type text/markdown, наряду со ссылками RSS и JSON Feed, так что агент, получивший HTML, может найти его, не зная пути.
То, что он подаёт, — это markdown, который мы составили, а вы одобрили, а не санитизированный HTML. Это безопасно как markdown, который инертен, и поэтому этот ответ никогда не бывает text/html. Если вы рендерите его сами, экранируйте его так же, как экранировали бы любой другой недоверенный markdown: записи, составленные из публичного репозитория, могут быть подвержены влиянию любого, кто может открыть там pull request.
Сбор обратной связи с вашего собственного сайта
Добавьте свои источники перед тестированием этого
Это единственный endpoint в продукте, который пишет, поэтому он не принимает запросы откуда угодно. Он сопоставляет заголовок Origin браузера со списком разрешений для каждой команды, и этот список начинается пустым. Пусто означает отклонить всё, а не разрешить всё. Пока вы не добавите источник, на который встраиваете, каждая отправка возвращается с 403 и {"error":"origin_not_allowed"}, и ничего не доходит до вашей папки входящих. Если ваша форма выглядит правильно и всё же не работает, почти всегда причина в этом. Установите список авторизованным PATCH к /v1/settings/feed с {"allowedOrigins": ["https://your-site.example"]}, и прочитайте его обратно с помощью GET по тому же пути, который отвечает вашим publicId, allowedOrigins, и feedTitle и feedDescription, которые ваши подписчики видят в читателе фидов. Мы храним каждый источник точно в той форме, в которой его отправляет браузер, так что завершающий слэш или явный порт по умолчанию в том, что вы отправляете, не проблема.
POST/v1/public/YOUR_PUBLIC_ID/feedbackPOST https://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/feedback
Content-Type: application/json
Origin: https://your-site.example
{ "email": "someone@example.com", "message": "Dark mode, please." }
202 Accepted
{ "publicSubmissionId": "0ZbQ8yqk3n7T1sVJ4mWpLd2rXfEuGh6A" }
email должен выглядеть как адрес email и содержать 254 символа или меньше. message должен быть непустым и содержать 2КБ или меньше, измеряемых в байтах UTF-8, а не символах. Тело JSON в целом ограничено 8КБ. Есть ещё одно поле, website: это ловушка для ботов, поэтому опустите его, или отправьте пустым, если рендерите его как скрытое поле так же, как это делает наш виджет.
Стоит понять ловушку для ботов, прежде чем отлаживать что-либо с её помощью. Если website приходит с чем-то написанным в нём, мы отвечаем 202 с совершенно обычным на вид ID отправки, а затем ничего не делаем, потому что бот, узнавший, что его поймали, просто пробует снова по-другому. Это правильный ответ для бота и запутывающий для вас, так что если в вашей собственной форме есть поле с именем website, которое браузер мог бы автозаполнить, переименуйте его или удалите. Отправка, которая выглядит принятой и никогда не появляется, почти всегда из-за этого.
Принятая нами отправка возвращает 202 с publicSubmissionId. Верните его отправителю и сохраните, если можете: это единственный способ, которым он может узнать, что произошло дальше.
Режимы сбоя: 400 с invalid_email или invalid_message для неверной формы, 413 с email_too_large или message_too_large для правильной формы, но слишком большой, 429 с rate_limited свыше 5 отправок в минуту или 30 в час с одного адреса на один фид, 403 с origin_not_allowed, и 404 с not_found для нераспознанного ID фида.
Также есть дневной лимит для каждой команды на то, сколько последующей работы могут запустить отправки. Сверх него мы всё равно принимаем и храним всё, что приходит, просто это ждёт, пока кто-то из вашей команды посмотрит, а не открывает что-либо самостоятельно.
Проверка одной отправки
GET/v1/public/YOUR_PUBLIC_ID/feedback/PUBLIC_SUBMISSION_IDОтвечает статусом, плюс githubIssueUrl, как только для этой отправки существует issue, плюс shippedEntry, несущий заголовок и ссылку, как только работа готова. Email отправителя никогда не читается из нашей базы данных для этого маршрута, тем более не возвращается, что делает ответ безопасным для рендеринга на странице, которую может увидеть кто угодно. ID — это все учётные данные целиком, относитесь к нему соответственно. Ограничен 20 запросами в минуту и 200 в час на адрес и фид.
Кэширование, CORS и условные запросы
Оба фида отправляют Cache-Control: public, max-age=60, stale-while-revalidate=300 вместе с сильным ETag. Отправьте этот ETag обратно как If-None-Match, и неизменённый фид отвечает 304 без тела. Ни одно поле ответа не несёт значения настенных часов, поэтому ETag остаётся стабильным, когда мы повторно рендерим данные, которые не изменились, что и делает эти 304 достойными доверия.
Оба фида и поиск отправки — это анонимные чтения, отвечающие с Access-Control-Allow-Origin: *, так что вы можете вызывать их с любого источника, из curl или из шага сборки. POST обратной связи — исключение: он отвечает вашим собственным разрешённым источником и Vary: Origin, никогда с подстановочным знаком. Браузеры делают preflight на нём, а preflight всегда отвечает 204 независимо от того, разрешён ли источник, поэтому его нельзя использовать для проверки ваших настроек.