OAuth
通过授权码流程,允许第三方应用代表 Warmbly 工作区执行操作。
API 密钥适用于你自己的脚本。如果你正在构建供其他人连接其 Warmbly 工作区的应用,应使用 OAuth:用户在同意授权页面向应用授予限定范围的访问权限,你将获得代表用户执行操作的令牌。整个过程中,你不会看到用户的密码或 API 密钥。
Warmbly 实现了带 PKCE 的 OAuth 2.1 授权码流程。机密客户端应用(在控制台注册)持有客户端密钥;公共客户端(原生、移动和 CLI 应用,包括自行注册的 MCP 客户端)使用 PKCE,无需客户端密钥。不支持隐式授权和密码授权。
注册应用
在控制台中打开设置 -> OAuth 应用,注册一个应用。需要提供:
- 名称,以及可选的说明、徽标和网站(将在同意授权页面显示)。名称需遵循命名规则,不得包含 Warmbly,包括插入空格或使用外形相似字符的写法,以免用户将应用误认为 Warmbly 官方应用。已经使用这类名称的应用可保留现有名称,直到名称被修改;
- 一个或多个重定向 URI(用户批准后返回的位置),必须精确匹配并使用 HTTPS,原生应用的回环 URL 除外;
- 应用可请求的权限范围(除
API_KEYS外的任意权限;该权限仅供用户和 API 密钥使用); - 可选的允许的 Webhook 域名(见下文)。
应用管理接口(/oauth/applications/* 和 /oauth/application-logo)仅供已登录成员或 API 密钥使用。使用 OAuth 访问令牌会被拒绝,返回 403 oauth_token_not_allowed,因此应用永远不能注册其他应用、轮换密钥或读取 Webhook 密钥。
你会获得客户端 ID和客户端密钥;密钥仅显示一次。请将密钥保存在服务端,用于验证令牌交换请求。
应用标志
应用徽标的工作方式与工作区徽标相同:上传到应用后,由 Warmbly 保存并设置。
POST /oauth/applications/{id}/logo接收多部分表单中的file:PNG 或 JPG 格式,最大 2 MB,宽高均需介于 32 至 1,024 像素。Warmbly 会解码图片并保存一份重新生成的副本,因此只保留像素,不保留相机、位置元数据或图片末尾的额外字节。响应为更新后的应用,旧徽标会被删除。DELETE /oauth/applications/{id}/logo用于移除徽标。
与应用的其他管理操作一样,两者均需 权限范围 API_KEYS 和组织权限 manage_api_keys。徽标会向所有看到同意授权页面或应用目录的工作区展示,因此创建和更新请求体中的 logo_url 只接受本实例为你的工作区签发的地址;其他地址会被拒绝,返回 400 invalid_logo。更新时不提供 logo_url 会保留当前徽标。
应用被停用或工作区被封禁时
实例运维人员可暂停应用。被暂停的应用无法让任何人登录,其访问令牌和刷新令牌均停止工作,Webhook 投递停止,并从社区目录中移除。应用的 suspended_at 和 suspended_reason 会反映这一状态,且只有运维人员可以解除暂停。自行停用应用时,行为相同,直到你再次启用它。
运维人员还可禁止某个工作区或用户注册、发布应用。GET /oauth/applications 会在 developer_access 中报告该情况(blocked、reason),后续注册或发布请求将返回 403 developer_access_blocked。工作区已有的应用会继续运行,除非这些应用也被暂停。
公开客户端与动态注册
原生应用、CLI 工具和 MCP 客户端无法安全持有机密值。因此它们以公共客户端身份连接,不使用客户端密钥,且必须使用 PKCE。有两种方式可获取公共客户端的 client_id。
在运行时通过动态客户端注册(RFC 7591)自行注册。MCP 客户端采用此方式,让用户只需一条命令即可连接:
curl -X POST "https://api.warmbly.com/v1/oauth/register" \
-H "Content-Type: application/json" \
-d '{
"client_name": "My MCP client",
"redirect_uris": ["http://127.0.0.1:8080/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"]
}'{
"client_id": "wmcid_...",
"client_id_issued_at": 1752624000,
"redirect_uris": ["http://127.0.0.1:8080/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "read_campaigns read_contacts ..."
}注册端点公开开放,无需身份验证,并按来源 IP 限流。注册本身不会授予任何访问权限,仍需用户登录并在同意授权页面批准权限范围。重定向 URI 可使用 HTTPS、回环地址(http://127.0.0.1 或 localhost,任意端口),或私有协议(myapp://);授权和令牌交换时均进行精确匹配。自行注册的客户端永远不能请求 SEND_CAMPAIGNS 或 API_KEYS。
自行注册的客户端没有任何身份担保,因此同意授权页面仅显示 Warmbly 能确认的信息。client_uri 和 logo_uri 会被忽略,页面不会显示其网站或徽标。client_name 会经过清理,而非直接拒绝:括号中包含 Warmbly 名称的部分会被删除(Claude Code (warmbly) 显示为 Claude Code);若清理后仍包含 Warmbly,或名称违反命名规则,则显示为 MCP client。
公共客户端在令牌交换时不发送 client_secret,只发送 PKCE 的 code_verifier。下文流程的其余部分完全相同。如果是在连接 AI 助手,无需手动执行这些步骤;单命令配置方式见 MCP 服务器 页面。
权限范围
OAuth 权限范围与 API 密钥的权限相同,只是使用小写名称,例如 read_campaigns、write_contacts。完整列表见权限参考。令牌携带以下三个集合的交集:
- 应用注册时声明的权限范围;
- 授权请求所请求的权限范围(
scope为空时,表示应用的全部权限范围); - 根据角色映射,批准授权的成员角色所覆盖的权限范围。
api_keys永远不会授予应用。
因此,应用向 Viewer(查看者)请求 write_contacts 时,只会获得该查看者持有的读取权限范围。同意授权页面会分别列出将授予的权限,以及已请求但超出成员角色范围的权限。令牌响应中的 scope 表示实际授予的集合,可能比请求范围更小(RFC 6749 第 3.3 节)。如果成员角色不包含任何请求的权限范围,同意授权页面会拒绝操作,返回 access_denied。
权限上限始终随成员角色变化,而不是只在同意授权时检查。每个携带令牌的请求都会根据成员的当前角色进行检查:降低其角色权限后,令牌也会失去新角色未涵盖的权限;调用所需权限已不再持有时,返回 403。刷新时,如果成员已不具备任何已授予的权限范围,则返回 invalid_grant。
授权流程
1. 将用户引导至授权页面
使用标准参数将用户浏览器重定向到同意授权页面。PKCE 可选,但推荐使用:生成 code_verifier(高熵随机字符串),并发送其 S256 挑战值。
code_challenge = base64url( sha256( code_verifier ) )https://app.warmbly.com/oauth/authorize
?response_type=code
&client_id=wmcid_...
&redirect_uri=https://yourapp.com/oauth/callback
&scope=read_campaigns%20read_contacts
&state=<random-csrf-token>
&code_challenge=<challenge>
&code_challenge_method=S256返回时应由你验证 state,以防范 CSRF。code_challenge 和 code_challenge_method 为可选项;如果提供,则 code_challenge_method 必须为 S256。
2. 用户批准授权
Warmbly 会显示应用、将被授予访问权限的工作区、将授予的权限范围、批准后访问权限的接收位置(重定向 URI 的主机;对于回环或私有协议重定向,则为本设备上的应用),以及授权或拒绝选项。除非应用是在控制台注册而非自行注册,且被实例列为社区目录中的精选应用,否则会标记为 Unverified app(未经验证的应用)。未经验证的应用还会显示 Not affiliated with Warmbly(与 Warmbly 无关联) 提示,要求成员仅批准自己主动发起的连接。由于批准会向应用交付持续有效的凭据,如果成员在最近几分钟内未登录或重新验证,批准时会要求其确认身份。批准后,浏览器会携带一次性代码重定向回 redirect_uri:
https://yourapp.com/oauth/callback?code=wmac_...&state=<your-state>如果用户拒绝,重定向会改为携带 ?error=access_denied&state=...。
3. 用授权码换取令牌
验证 state 一致后,向令牌端点发起 POST 请求。请求类型为 application/x-www-form-urlencoded;发送 client_secret(放在请求体中,或通过 HTTP Basic 身份验证传入),如果使用 PKCE,还需发送原始的 code_verifier。公共客户端省略 client_secret,并始终发送 code_verifier。
curl -X POST "https://api.warmbly.com/v1/oauth/token" \
-d grant_type=authorization_code \
-d code=wmac_... \
-d redirect_uri=https://yourapp.com/oauth/callback \
-d client_id=wmcid_... \
-d client_secret=wmcs_... \
-d code_verifier=<the-original-verifier>{
"access_token": "wmat_...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "wmrt_...",
"scope": "read_campaigns read_contacts"
}4. 调用 API
访问令牌的使用方式与 API 密钥完全相同:
curl "https://api.warmbly.com/v1/campaigns" \
-H "Authorization: Bearer wmat_..."5. 令牌到期后刷新
访问令牌有效期为一小时。可在过期前后用刷新令牌换取新的一对令牌。刷新令牌会轮换:新令牌对一经签发,旧刷新令牌立即失效,因此务必保存最新令牌。提交已交换过的刷新令牌,例如两个工作节点并行刷新、使用旧令牌重试或重放,会撤销该授权,用户必须重新授权(RFC 9700 第 4.14.2 节)。同一授权的刷新操作必须串行执行。
curl -X POST "https://api.warmbly.com/v1/oauth/token" \
-d grant_type=refresh_token \
-d refresh_token=wmrt_... \
-d client_id=wmcid_... \
-d client_secret=wmcs_...撤销
用户断开连接时,应撤销访问令牌或刷新令牌及其关联授权:
curl -X POST "https://api.warmbly.com/v1/oauth/revoke" \
-d token=wmat_... \
-d client_id=wmcid_... \
-d client_secret=wmcs_...用户也可以自行撤销应用授权;具有工作区凭据管理权限(manage_api_keys 或 manage_settings)的成员,可在设置 -> OAuth 应用 -> 已授权应用下撤销任何成员的授权。两种操作都会使该成员授权签发的所有令牌失效。用户被移出工作区时,其在该工作区授予的所有授权也会以相同方式失效,因此下一次刷新预计会返回 invalid_grant。
工作区授权
这些端点仅接受会话,供管理工作区凭据的人员使用。两者均需要组织权限 manage_api_keys 或 manage_settings。
GET /oauth/workspace-authorizations返回{"authorizations": [...]}:每个应用与成员组合对应一行,包含application_id、name、logo_url、website_url、user_id、user_email、user_name、scopes、authorized_at和last_used_at。只列出仍有效的授权,即未撤销且刷新令牌未过期的授权。DELETE /oauth/workspace-authorizations/{application_id}/members/{user_id}撤销该成员对该应用的授权,并返回{"revoked": true}。没有可撤销内容时也会成功,因此可以安全重试。
令牌有效期
| 令牌 | 有效期 | 说明 |
|---|---|---|
| 授权码 | 10 分钟 | 一次性使用,绑定 PKCE |
| 访问令牌 | 1 小时 | Bearer 令牌,携带已授予的权限范围,并受成员当前角色限制 |
| 刷新令牌 | 90 天 | 每次使用都会轮换;重复使用旧令牌会撤销授权 |
刷新令牌已过期的授权不再计为一次安装:不会接收应用 Webhook,不计入目录安装数,也不会出现在已授权应用列表中。
发现机制
端点 URL 和支持的能力会公开发布,供客户端自动配置。
授权服务器元数据(RFC 8414):
GET https://api.warmbly.com/.well-known/oauth-authorization-server其中列出授权、令牌、撤销和注册端点、支持的权限范围、唯一的响应类型 code、authorization_code 和 refresh_token 授权类型、唯一的 PKCE 方法 S256,以及客户端身份验证方式 none(公共客户端)、client_secret_basic 和 client_secret_post。
MCP 端点的受保护资源元数据(RFC 9728):
GET https://api.warmbly.com/.well-known/oauth-protected-resource其中标明资源及其授权服务器,因此 MCP 客户端从 /v1/mcp 收到带 WWW-Authenticate 质询的 401 响应时,可以发现应在哪里进行身份验证。请参阅 MCP 服务器 页面。
OAuth 应用的 Webhook 域名
如果应用代表用户注册 Webhook 端点,必须声明这些端点可使用的域名。创建或更新应用时,在应用对象上设置 allowed_webhook_domains(字符串数组):
{
"name": "My App",
"redirect_uris": ["https://yourapp.com/oauth/callback"],
"allowed_webhook_domains": [".acme.com", "hooks.partnersite.com"]
}应用注册的任何 Webhook URL,其主机都必须位于此列表允许的范围内。空列表表示应用完全不能注册 Webhook 端点。
匹配规则:
- 不带前导点的条目(
acme.com)仅匹配完全相同的主机。 - 带前导点的条目(
.acme.com)匹配根域名及任意子域名:acme.com、hooks.acme.com、prod.hooks.acme.com。
端点注册时会强制检查此列表,投递时也会重新检查。完整的端点验证和投递流程见Webhook 参考。
应用接收事件(Webhook)
应用无需为每个安装它的工作区保持一条 WebSocket 连接,而可以声明一份 Webhook 配置,让 Warmbly 自动将每个授权组织的事件投递到该地址。这采用 GitHub / Slack 应用的模式:在应用上配置一次,即可接收所有安装组织的事件。
应用 Webhook 配置
通过 POST /oauth/applications 或 PATCH /oauth/applications/:id 创建或更新应用时,设置 webhook_url 和 webhook_events。
| 字段 | 类型 | 说明 |
|---|---|---|
webhook_url | string | 接收事件投递的 HTTPS URL。其主机必须位于应用的 allowed_webhook_domains 允许范围内,否则返回 400。设为空值可停止应用级 Webhook 投递。 |
webhook_events | string[] | 应用订阅的事件名称。数组为空或省略时,订阅组织所授予权限范围允许的全部非全量流式事件。 |
签名密钥由服务器生成,整个应用共用一个密钥,用于签署来自所有安装组织的投递。与各端点自身的副本一样,它使用实例凭据密钥加密存储。使用下方专用端点查看或轮换密钥。
{
"name": "My App",
"redirect_uris": ["https://yourapp.com/oauth/callback"],
"allowed_webhook_domains": [".yourapp.com"],
"webhook_url": "https://hooks.yourapp.com/warmbly",
"webhook_events": ["campaign.reply_received", "meeting.booked", "contact.created"]
}根据权限范围自动投递
组织授权应用后,Warmbly 会自动为该组织创建受管理的 Webhook 端点,并开始投递订阅的事件。组织撤销应用授权后,该端点会被移除。
可投递事件取决于应用在 webhook_events 中声明的事件,与组织所授予权限范围允许事件的交集。只有应用持有相应权限范围且组织也已授予该权限时,才会投递该事件类别。权限范围到事件的映射如下:
| 事件 | 所需权限范围 |
|---|---|
inbox.* | READ_UNIBOX |
campaign.*, deliverability.*, meeting.* | READ_CAMPAIGNS |
email_account.*, warmup.* | READ_EMAILS |
contact.*, bulk_operation.* | READ_CONTACTS |
crm.* | READ_CRM |
automation.* | INTEGRATIONS |
team.*, role.*, settings.*, subscription.* | READ_AUDIT_LOGS |
custom.event | REALTIME_SUBSCRIBE |
每次投递的载荷都包含 organization_id,便于识别事件来自哪个安装组织。投递会签名,并按普通 Webhook 相同的退避计划重试,可使用同样的 X-Warmbly-Signature 机制验证。验证算法见Webhook:验证签名。
应用 Webhook 管理端点
四个端点均需 权限范围 API_KEYS 和组织权限 manage_api_keys。
查看 Webhook 密钥
GET /oauth/applications/{id}/webhook-secret
返回当前签名密钥。该值带 whsec_ 前缀,用于验证此应用在所有安装组织中的投递。
{ "webhook_secret": "whsec_9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a0" }轮换 Webhook 密钥
POST /oauth/applications/{id}/webhook-secret/rotate
签发新的签名密钥,并仅返回一次。在使用旧密钥签名的在途投递完成前,应更新验证逻辑。
{ "webhook_secret": "whsec_1a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f7081" }列出各组织的端点
GET /oauth/applications/{id}/webhook-endpoints
返回 Warmbly 为此应用实际创建的各组织受管理端点,以及当前健康状态。
{
"endpoints": [
{
"id": "a3f1c2e4-5b6d-7e8f-9a0b-1c2d3e4f5a6b",
"organization_id": "11111111-2222-3333-4444-555555555555",
"url": "https://hooks.yourapp.com/warmbly",
"enabled": true,
"verified_at": "2026-06-14T10:00:00Z",
"consecutive_failures": 0,
"last_success_at": "2026-06-14T10:05:00Z",
"last_failure_at": null
}
]
}跨组织投递日志
GET /oauth/applications/{id}/webhook-deliveries
返回所有安装组织的投递日志,按最新优先排序。每次尝试均包含响应码、错误、载荷和重试状态。
| 参数 | 位置 | 类型 | 说明 |
|---|---|---|---|
status | query | string | 按状态筛选:pending、in_flight、delivered、failed 或 abandoned。 |
event_type | query | string | 按事件类型筛选,例如 campaign.reply_received。 |
limit | query | integer | 最大行数。范围为 1 至 200,默认 50。超出范围会返回 400。 |
cursor | query | string | 来自 pagination.next_cursor 的不透明分页游标。 |
响应采用标准的 data + pagination 封装和不透明游标。每行都包含 organization_id,便于查看各次投递所属的安装组织。
Webhook 与实时网关的区别
实时网关和应用级 Webhook 使用同一套事件名称。请根据集成需求选择:
| 实时网关 | 应用级 Webhook | |
|---|---|---|
| 传输方式 | WebSocket(持久连接) | HTTP POST(无状态投递) |
| 适用场景 | 实时界面、在线状态、低延迟仪表板 | 持久化处理、数据管道、无服务器接收端 |
| 连接模式 | 每个安装组织一条连接 | 无需持久连接;Warmbly 向你的 URL 推送 |
| 投递保证 | 断开连接时可能丢失事件,支持恢复和重放 | 指数退避重试,可查看日志 |
| 权限范围检查 | 按连接的订阅意图检查 | 按应用权限范围检查,并对每个组织的授权强制执行 |
两种方式均可采用,取决于所构建的功能。类似仪表板的集成适合使用 WebSocket 的低延迟和在线状态功能。数据管道或无服务器集成则适合 Webhook 的持久化 HTTP 投递:每次投递都可单独检查,也无需为每个安装组织保持连接。
发布到社区目录
应用正常运行后,可发布到社区目录。在设置 -> OAuth 应用中使用应用的发布操作,或调用以下端点。
已发布的目录条目具有 status:
published:仅通过链接访问。可在/app/integrations/apps/{slug}打开,并提示该应用通过链接分享;在发布者以外的至少 25 个工作区安装之前,不会出现在目录、搜索或推荐中,且这些工作区各自都必须已创建至少两周。featured:由实例团队选为精选。无论安装数多少,都会出现在目录中并带有精选标记。hidden:团队已将其下架。任何位置均不可访问,status_note会说明原因。
对目录条目,或应用名称、徽标、网站、权限范围的任何修改,都会使 featured 条目恢复为 published。没有实际变更的保存会保留状态;hidden 条目在编辑后仍保持隐藏。
目录中的安装操作会打开 install_url。从该地址使用你自己的 state 启动授权码流程,让用户像从你的网站发起连接一样,进入 Warmbly 同意授权页面。
三个端点均需 权限范围 API_KEYS 和组织权限 manage_api_keys。
获取目录条目
GET /oauth/applications/{id}/listing
返回 {"listing": {...}};应用尚未发布时返回 {"listing": null}。
发布或编辑目录条目
PUT /oauth/applications/{id}/listing
替换整个目录条目,因此可以安全地重复请求。
| 字段 | 类型 | 说明 |
|---|---|---|
slug | string | 链接标识。由 3 至 48 个小写字母、数字或单个连字符组成,首尾必须为字母或数字。在实例内唯一;内置集成的名称为保留名称。 |
tagline | string | 单行,最多 120 个字符。 |
description | string | 可选,最多 2,000 个字符。保留换行;标记语法按普通文本显示。 |
category | string | 取值为 crm、automation、notifications、meetings、data、verification、ai、other 之一。 |
install_url | string | 点击安装后前往的地址。必须使用 https。 |
support_url | string | 可选的 https 链接。 |
privacy_url | string | 可选的 https 链接。 |
{
"listing": {
"application_id": "5a8f0c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
"organization_id": "11111111-2222-3333-4444-555555555555",
"slug": "acme-sync",
"tagline": "Push positive replies into Acme as deals",
"description": "",
"category": "crm",
"install_url": "https://acme.com/warmbly/install",
"support_url": "https://acme.com/support",
"privacy_url": "",
"status": "published",
"submitted_at": "2026-10-03T09:00:00Z",
"created_at": "2026-10-03T09:00:00Z",
"updated_at": "2026-10-03T09:00:00Z"
}
}可能的拒绝响应包括 400 invalid_listing(message 指明字段)、应用已停用或动态注册时的 400 app_not_listable,以及 409 listing_slug_taken。请参阅错误码。
取消发布
DELETE /oauth/applications/{id}/listing
移除目录条目及其链接。已安装应用的工作区保留其授权,直到自行撤销。不存在目录条目时也会成功,因此可以安全重试。
浏览目录
GET /integrations/community 返回目录中列出的应用:精选应用优先,其后是已在发布者以外至少 25 个工作区安装、且这些工作区各自已创建至少两周的应用,按安装数从多到少排序。响应使用 data 加 pagination(limit 范围 1 至 200,默认 100)。GET /integrations/community/{slug} 返回一个已发布或精选条目,无论它是否出现在目录中。两者均需 权限范围 INTEGRATIONS;每个条目都会报告 installs(持有有效授权的工作区数)和 installed(调用者工作区是否持有有效授权)。请参阅集成。
安全说明
- 令牌的操作永远不会超出批准授权成员的当前角色权限,且永远不能管理 API 密钥或 OAuth 应用。
- 重定向 URI 采用精确匹配,因此应注册所有使用的回调地址。
- 始终发送并验证
state。 - 公共客户端必须使用 PKCE,机密客户端应用也建议使用;启用时,仅接受
S256。 - 将客户端密钥保存在服务端。原生、移动和 CLI 应用应注册为公共客户端,使用 PKCE,不应嵌入密钥。
- 动态注册公开开放,并按 IP 限流;自行注册的客户端属于公共客户端,不能请求
SEND_CAMPAIGNS或API_KEYS,并且只有用户在同意授权页面批准权限范围后才会获得访问权限。 - 将
allowed_webhook_domains限制为应用所需的最小域名集合。如果用户诱导应用注册非预期 URL,过宽的列表会扩大 SSRF 风险范围。 - 应用 Webhook 密钥由所有安装组织共享。如果密钥泄露,应轮换并及时更新接收端:已经签名的在途投递在完成前仍需使用旧密钥验证。