开发者文档
最后更新于 2026 年 9 月 26 日。
Changeloop 为你发布的一切都是通过 HTTPS 传输的简单 JSON。无需安装 SDK,无需轮换 API 密钥,也无需登录:下方两个 feed 都是以你的 feed ID 为键的匿名公开读取接口。请在本页任意示例中将 YOUR_PUBLIC_ID 替换为你自己的 ID。
开始之前有一点需要了解:你的公开 feed ID 就在应用本身中。登录后打开“设置”,就在默认进入的“公开 Feed”部分中,那里还有现成的 changelog.json 和 roadmap.json 链接、你托管的 feed 页面链接,以及下方的 widget 代码片段,每一个都有各自的复制按钮。
快速开始
从注册到在你自己的网站上展示 changelog,只需五步。应用里的“Get started”页面会一步步带你完成,并在每一步完成后自动打勾。
- 连接一个来源:GitHub 仓库、GitLab 项目或 Bitbucket 仓库。
- 选择条目使用的语言。
- 可选:创建标签,方便读者按产品模块筛选。
- 发布你的第一个条目。合并的更改会以草稿形式进入审核收件箱:批准其中一个,或为该仓库开启自动发布。
- 放到你的网站上:链接到托管页面、粘贴 widget,或在你自己的页面中渲染 JSON feed。
用大约十行 React 代码构建你的 changelog
把这段代码粘贴到组件中,你就拥有了一个可用的 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 feed
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。
未知的 feed ID 会返回带有 {"error":"not_found"} 的 404,格式错误的 ID 同样如此。两者被刻意设计为无法区分,因此该端点无法用于探测哪些 ID 存在。
roadmap feed
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 编号,这是刻意为之,而不是我们日后会补上的遗漏。
该端点完全不接受任何查询参数。没有游标、没有 limit,也没有仓库过滤器,因为 roadmap 是一个由人工整理的小型看板,而不是一个无限增长的日志。每一列最多返回 50 个项目,如果实际数量更多则设置 hasMore。hasMore 仅供参考:没有可以跟随的游标,所以不要围绕它构建分页。
publicTitle 和 publicDescription 是从 issue 的标题和正文生成的纯文本,在公开仓库中可能会受到任何能打开 issue 的人的影响。它们不带任何 HTML 净化保证,也不是 htmlContent 的例外。请将它们作为纯文本渲染。
可嵌入的 widget
如果你不想自己构建任何东西,插入这两行代码即可。widget 是一个渲染在 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 是你的 feed ID,data-api 是 widget 获取数据的来源。如果缺少任意一个,该元素会在控制台写入错误并且不渲染任何内容——如果你看到本应显示 widget 的地方是一片空白,这是首先要检查的事项。
给元素加上 data-theme="dark" 即可使用深色渲染;你的页面可以在运行时切换。若要更深入地定制样式,widget 提供了 CSS 自定义属性(--changelogapp-text、--changelogapp-bg、--changelogapp-accent 等)和 ::part() 名称,在你自己的样式表中设置即可。应用会在设置的“公开 Feed”中实时预览两种主题。
添加 data-repos 可以只展示部分仓库,例如多个产品共用一个账户时,在某个产品的网站上只展示该产品的 changelog。值是用逗号分隔的完整名称列表,格式为 owner/repo;不带所有者的名称不会匹配任何内容,并且会无提示地显示为空的动态。最多支持十个仓库。限定范围后的小组件只显示 Updates 和 Feedback 两个标签页,因为路线图没有按仓库划分的视图;反馈仍会提交到团队反馈目标所指向的位置。在“设置,公开动态”中有一个选择器,可以替你写好这个属性。
它按以下顺序渲染三个标签页:Updates、Roadmap 和 Feedback。前两个读取上面的 feed。第三个会提交到下方的端点,并将每次提交的 ID 保存到 localStorage 中,这样访问者可以回来查看他们提交内容的处理情况。
该脚本按版本提供。/widget.js 始终提供最新构建,并缓存一小时,因此新版本无需你做任何操作即可到达访问者。/widget-vN.js 固定某一个构建:一旦某个版本号被提供过,其字节内容将永不改变,并缓存一年。如果你希望有意地采用变更,请使用固定版本。
每个页面只加载一个 widget 脚本
这两个 URL 是替代方案,不是叠加层。两者都注册相同的自定义元素名称,而浏览器只允许每个文档注册一次某个名称:先执行的脚本会在该页面的整个生命周期内胜出,第二个则会失效。因此同时包含 /widget.js 和 /widget-v5.js 的页面会渲染浏览器先执行的那一个,这不是你能控制的,在已有 /widget.js 旁边添加 /widget-v5.js 来固定版本不会产生任何效果。
发生这种情况时,widget 会在控制台写入一条同时指出两个构建的警告,因此你不必猜测。它只能给出警告:等到第二份副本执行时,第一份已经占用了该名称。解决方法始终是替换脚本标签,而不是再添加一个,如果是标签管理器或某个片段代替你插入的脚本也同样适用。要从持续更新的构建切换到固定构建,请更改 src。
托管的 feed 页面
https://feed.changeloop.dev/feed/YOUR_PUBLIC_ID我们也在该地址托管了一个简单的页面:你的 changelog 和 roadmap 看板,均从上面相同的两个 feed 渲染而来。它不需要登录,也不需要你做任何配置。当一个反馈闭环完成时,这里也是我们将人们导回的地方:我们在 GitHub issue 上留下的 Shipped 评论会链接到这里,上面提交查询中的 shippedEntry.link 也是如此,两者都会指向已交付条目自身的 #entry-ID 锚点,即使该条目后来移到了后面的页面,这个锚点仍然能找到它。
请将其视为备用方案,而非集成方式。将其放在你自己的网站上、让它看起来像你的产品而不是我们的产品,方法仍然是 changelog feed 和 widget;这个页面是为了你尚未这样做的情况,也是为了无论你还构建了什么,都会指向这里的反馈闭环链接。
你自己的域名
你可以从自己的地址提供托管页面,无需改动 DNS 或证书。在设置的“自定义域名”中粘贴读者将看到的公开地址(例如 https://example.com/changelog),然后把你网站上的这个路径指向那里显示的代理目标:一条规则即可覆盖页面、资源、数据和 feed。“检查我的域名”会从我们这边访问你的地址,告诉你代理是否正确,若不正确则说明需要改什么。
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 正文。feed 提供的 HTML 是由我们的净化器从你的 markdown 重新渲染而来;代理无法提供 HTML。
- approve_entry - 发布。这是公开且即时的,并会通知 GitHub 上任何已关联的反馈。只有待处理的条目才能被批准。
- discard_entry - 将条目排除在 changelog 之外。可从网页应用中撤销。
- get_changelog_info - 你的 feed ID 以及提供你 changelog 的地址。
它做不到的事
每个工具都限定在该密钥所属的团队范围内,且没有任何一个工具接受团队作为参数,因此即使有什么东西尝试,也没有办法指向另一个团队。服务器不接受浏览器会话,只接受密钥:请求必须有意地附带凭据。而且密钥无法管理密钥,也无法下载数据导出,因此以这种方式连接的代理无法为自己签发第二个凭据,也无法在一次调用中提取你的数据。
API 密钥
以上内容都是匿名的,不需要任何凭据。经过身份验证的 API——你的设置、你的审核收件箱——是另一个界面,它既接受已登录的浏览器会话,也接受 API 密钥。密钥是为脚本和代理准备的:任何需要在没有人操作键盘的情况下访问你的 changelog 的东西。
Authorization: Bearer clapi_YOUR_KEY在应用的“设置”下的“API 密钥”标签页中创建一个。密钥只会在你创建它的那一刻显示一次,之后就不会再显示:我们只存储它的哈希值,因此没有任何界面能第二次向你展示它。如果丢失了,请将其撤销并创建另一个。
密钥能做什么,不能做什么
密钥拥有与登录相同的访问权限,限定在它被创建的那一个团队内,但有两个刻意的例外。它无法管理 API 密钥,也无法下载数据导出。这两者都需要真正的登录,这样一旦密钥泄露,也无法为自己签发替代品,无法撤销你用来锁定它的密钥,也无法在一次请求中提取你团队的数据。
撤销
撤销会在下一次请求时生效。被撤销的密钥会像未知密钥一样返回 401,即使是在仍持有有效会话的浏览器中也会持续返回 401,因为带有 Authorization 请求头的请求永远不会被悄悄地重试为 cookie 请求。被撤销的密钥仍会保留在列表中,标注撤销日期和最后使用日期,这正是你在追查泄露密钥的影响范围时所需要的信息。
套餐与限额
免费套餐每月撰写 20 条已合并的变更,并把每日查看的合并、分类的反馈、起草的路线图卡片和替代版本各限制为 50 条;团队套餐没有硬性上限。设置的“套餐与用量”会按产品自身的计数方式显示每项预算及其重置时间,在任何请求被拒绝之前就能看到。超出限额到达的工作会被暂存而非丢失:超出配额的条目会在收件箱中等待,被拒绝的路线图草稿在窗口重置后可以重试。
GitLab 和 Bitbucket
GitLab 项目或 Bitbucket 仓库可以像 GitHub 仓库一样为你的 changelog 提供内容:在“设置”中的 GitLab 或 Bitbucket 页面连接,添加我们提供的 webhook(在 bitbucket.org 上,如果 Bitbucket 页面提供该按钮,也可以交给 Connect with Bitbucket 添加),之后每一个合并到你指定分支的更改都会以相同方式撰写、经过相同的人工审核,成为你审核收件箱中的一条草稿条目。条目来自已合并的 pull request 或 merge request;在 GitHub 和 Bitbucket 上,如果你在“设置”中的 What creates drafts 选择推送模式,也可以来自推送。GitLab 项目只会根据 merge request 起草。
连接一个项目
GitLab 项目在“设置”中的 GitLab 页面连接,Bitbucket 仓库在“设置”中的 Bitbucket 页面连接。输入路径(GitLab 上是 acme/web 这样的组和项目,Bitbucket 上是 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 仓库后重新连接,请同时在 Bitbucket 上依次进入 Repository settings、Webhooks,删除旧的 webhook。项目连接后,你可以在该项目所在的行里更改分支并开启自动发布;如果某次投递被忽略,该行会说明原因。
为什么 Bitbucket 会询问分支而 GitLab 不会
GitLab 会告知我们你的项目将哪个分支视为默认分支,因此你可以将该字段留空,这样做正是那个意思。Bitbucket 完全不会发送默认分支,因此如果我们允许你留空,我们将没有任何可以比对的对象,你的 webhook 会看起来安装得很完美,却始终产生不了一条条目。我们宁愿多问一个问题,也不愿让这种情况发生。使用 Connect with Bitbucket 时,我们会在你授权访问时向 Bitbucket 查询主分支,因此你无需手动输入。
它们尚未覆盖的内容
只有 changelog 条目,别无其他。为你提交 issue 的反馈 widget、修复交付后回复到该 issue 的评论、由 issue 标签驱动的公开 roadmap,以及审核收件箱中的源代码预览,目前都仅限 GitHub 使用。
原因是我们宁愿直说,也不愿含糊其辞。每一项功能都需要一个由我们保管的、对你的项目具有写入权限的访问令牌。changelog 条目不需要任何这样的令牌,因为它们撰写所依据的一切都随 webhook 本身到达,因此通过 webhook 连接 GitLab 或 Bitbucket 不会向我们提供任何凭据,也不会让我们读取你的代码。Connect with Bitbucket 是唯一的例外。Bitbucket 仅为单次请求向我们借出一个可以读取该仓库及其 pull request 并管理其 webhook 的令牌,我们只用它读取主分支并添加 webhook,随后即将其丢弃。我们不保存任何内容。我们宁愿交付那个不花费你任何成本的部分,也不愿为了凑齐功能列表而要求提供令牌。
条目的其他版本
一次变更通常需要被解释不止一次:在 changelog 中告诉客户,向回答相关问题的人说明,以及在没有人会读四段文字的频道里传达。在审核收件箱中,你可以在批准条目之前,为它撰写两个额外版本中的一个。
公告版本只有一到两行,是你批准条目时发送到 Slack 的内容,用来代替完整文本。支持说明是一份内部简报:发生了什么变化、客户会注意到什么,以及一句支持人员几乎可以原样说出的话。这两者都是可以在使用前重写的草稿,也都可以被删除。
两者都不会被发布
这些版本永远不会出现在你的 changelog 页面、任何 feed、widget 或提供它们的 API 中。支持说明尤其是为你公司内部的人撰写的,可能比条目本身更为直接。它存在的唯一地方是你的审核收件箱,以及如果你使用的话,你自己保存的副本。
它们的撰写依据
始终依据条目本身撰写,绝不依据 pull request。这是刻意为之:条目已经经过了让安全修复保持模糊的规则,也经过了你自己的审核。从条目重写的版本无法重新引入你已删除的细节,因为该细节根本不在给模型的输入之中。
在 Slack 中发布公告
批准一条条目后,它可以在公开发布的同一时刻发布到 Slack 频道。在“设置”的“Slack”标签页中连接:在你自己的工作区创建一个传入 webhook,选择频道,粘贴 URL。除了那个 webhook,你这边不会安装任何东西,我们也不会请求访问你工作区的任何权限。
该消息包含条目标题、你批准时的文本、其分类和标签,以及指向你 changelog 中该条目的链接。Markdown 会被转换为 Slack 实际渲染的样式,因此条目不会带着自己的星号到达。
webhook 的 URL 是一种凭据
任何持有该 URL 的人都可以向该频道发布内容,因此我们像对待密码一样对待它:一旦存储,之后任何界面、任何 API 响应——包括你自己的数据导出——都不会再显示它。之后你看到的是一个掩码,足以区分两个 webhook,但对其他任何人都没有用。我们只接受 hooks.slack.com 地址,因此拼写错误或被替换的 URL 会被拒绝,而不会被获取。
它停止工作的时候
如果你在 Slack 中移除该应用或归档该频道,webhook 会永久停止工作。我们会在第一条被拒绝的消息处注意到这一点,并关闭公告功能,同时在 Slack 标签页中标注原因和日期。我们刻意不选择默默持续重试:一个没有人公告过的 changelog,看起来与一个没有人读过的 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——将相同的已发布条目作为可订阅的 feed 提供。两者都接受与 changelog feed 相同的 repos、category 和 tag 过滤器,并带有相同的 Cache-Control 和 ETag。两者都不分页:读者轮询的是 feed 的最新内容,因此这些接口只返回最新的条目,没有游标。
条目文本是经过净化的 HTML,在 RSS 中包裹在 CDATA 中,在 JSON Feed 中则作为 content_html 提供。JSON Feed 还会在一个带命名空间的 _changelogapp 扩展下附带你的标签颜色;RSS 则不会,因为没有阅读器会去渲染这些颜色。
托管页面将两者都以 rel="alternate" 链接的形式对外声明,因此浏览器或阅读器落到该页面上时,无需被告知具体路径也能订阅。
单独的一个条目
GEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/entries/ENTRY_ID返回单个已发布条目,与 changelog feed 在其 data 数组中所携带的是同一个对象。feed 中的永久链接指向的正是这里,当你已有一个 ID、又不想通过翻阅 feed 来找到它时会很有用。未知的 ID,或属于未发布条目的 ID,会返回 404,返回的内容体与其他任何未知 ID 相同。
markdown feed
GEThttps://api.changeloop.dev/v1/public/YOUR_PUBLIC_ID/changelog.md以纯 markdown 的形式提供相同的已发布条目,作为 text/markdown 提供。它是为非浏览器的读者而存在的:回答“这个产品最近有什么变化”的 LLM 或代理无需解析 RSS 或遍历 JSON 即可获得文本内容。它接受与 changelog feed 相同的 repos、category 和 tag 过滤器,带有相同的 Cache-Control 和 ETag,并与另外两个接口完全一样地对条件请求返回 304。
每个条目都是一个小节:标题作为标题行,之后是包含日期、分类和任何标签的单独一行,然后是按撰写原样呈现的条目文本,之后是条目的 Learn more 链接(如果有),最后是它的永久链接。文档以你的 feed 标题和描述开头,并链接回托管页面。当尚无任何内容发布时,它会用一句话说明这一点,而不是返回空的内容体,这样读者就能将其与一次失败的请求区分开来。
托管页面会以 type text/markdown 的形式将其声明为 rel="alternate" 链接,与 RSS 和 JSON Feed 链接并列,因此已获取 HTML 的代理无需被告知路径也能找到它。
它提供的是我们生成、你已批准的 markdown,而不是经过净化的 HTML。作为 markdown 它是安全的,因为 markdown 本身是惰性的,这也是这个响应永远不会是 text/html 的原因。如果你自己渲染它,请像转义任何其他不受信任的 markdown 一样对其进行转义:从公开仓库生成的条目可能会受到任何能在那里发起 pull request 的人的影响。
从你自己的网站收集反馈
测试之前请先添加你的来源
这是产品中唯一一个执行写入操作的端点,因此它不会接受来自任意地方的请求。它会将浏览器的 Origin 请求头与按团队维护的允许列表进行比对,该列表一开始是空的。空列表意味着拒绝一切,而不是允许一切。在你添加了你所嵌入的来源之前,每一次提交都会返回带有 {"error":"origin_not_allowed"} 的 403,你的收件箱不会收到任何内容。如果你的表单看起来正确却依然失败,几乎总是这个原因。请通过一个经过身份验证的 PATCH 请求,向 /v1/settings/feed 发送 {"allowedOrigins": ["https://your-site.example"]} 来设置该列表,并通过对同一路径发送 GET 请求读回,该请求会返回你的 publicId、allowedOrigins,以及你的订阅者在 feed 阅读器中看到的 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 必须看起来像一个邮箱地址,且长度不超过 254 个字符。message 不能为空,且不超过 2KB(以 UTF-8 字节而非字符计算)。整个 JSON 正文上限为 8KB。还有一个字段,website:这是一个蜜罐字段,因此请省略它,或者如果你像我们的 widget 那样将其渲染为隐藏输入框,请将其留空提交。
在用它调试任何东西之前,理解这个蜜罐字段是值得的。如果 website 带着某些内容到达,我们会返回一个看起来完全正常的提交 ID 和 202,然后什么都不做,因为得知自己被抓的机器人只会换一种方式再次尝试。这对机器人来说是正确的响应,但对你来说会造成困惑,因此如果你自己的表单中有一个名为 website、可能被浏览器自动填充的字段,请重命名或移除它。看起来被接受却始终不出现的提交,几乎总是因为这个原因。
我们接受的提交会返回带有 publicSubmissionId 的 202。请将其返回给提交者,并尽可能保存下来:这是他们了解后续处理情况的唯一方式。
失败模式包括:格式错误时返回 400,带有 invalid_email 或 invalid_message;格式正确但过大时返回 413,带有 email_too_large 或 message_too_large;单个地址向单个 feed 每分钟超过 5 次或每小时超过 30 次提交时返回 429,带有 rate_limited;返回 403,带有 origin_not_allowed;以及对于无法识别的 feed ID 返回 404,带有 not_found。
此外还有一个按团队设置的每日上限,用于限制提交能够触发多少下游工作。超过该上限后,我们仍会继续接受并存储所有到达的内容,只是它会等待你团队中的某个人查看,而不会自行打开任何内容。
检查一次提交
GET/v1/public/YOUR_PUBLIC_ID/feedback/PUBLIC_SUBMISSION_ID以 status 响应,一旦该提交存在对应的 issue,会附带 githubIssueUrl,一旦工作完成,还会附带包含标题和链接的 shippedEntry。提交者的邮箱地址永远不会因这条路由而从我们的数据库中被读取,更不会被返回,这使得该响应可以安全地渲染在任何人都能看到的页面上。该 ID 就是全部的凭据,请将其如此对待。它按地址和 feed 限制为每分钟 20 次请求、每小时 200 次请求。
缓存、CORS 和条件请求
两个 feed 都会发送 Cache-Control: public, max-age=60, stale-while-revalidate=300,并带有一个强 ETag。将该 ETag 作为 If-None-Match 送回,未变化的 feed 会返回不带正文的 304。任何响应字段都不携带挂钟时间的值,因此在我们重新渲染未发生变化的数据时,ETag 会保持稳定,这也是这些 304 值得信赖的原因。
这两个 feed 和提交查询都是匿名读取接口,会以 Access-Control-Allow-Origin: * 响应,因此你可以从任何来源、从 curl 或从构建步骤中调用它们。反馈的 POST 是例外:无论来源是否被允许,它都会以你自己被允许的来源和 Vary: Origin 响应,而不是通配符。浏览器会对其进行预检,而预检始终返回 204,无论来源是否被允许,因此它无法被用来探测你的设置。