概览
API 概览
Sofia API v1 的所有端点及其参数、权限范围和示例,并提供可用你自己的密钥逐个试用的 Playground。
基础 URL https://api.sofia-post.com/api · OpenAPI 文档
Sofia API 让你的代码访问你的 Sofia 账号:你的 Workspace 中的商家、它们已连接的平台、媒体库、帖子和统计数据。这是基于 HTTPS 的 REST API;请求和响应都是 JSON。它可以让你:
- 发布:立即发布、定时发布或存为草稿,可同时发往一个或多个平台。
- 发布前校验:通过模拟告诉你每个平台是否会接受这条帖子,不会写入任何内容。
- 管理媒体:上传文件,或从 URL 导入到商家的媒体库中。
- 跟踪帖子:读取每个平台的状态、永久链接或失败原因。也可以通过已签名的 Webhook 通知你。
- 读取统计数据:Sofia 为每个已连接账号收集的最新数据。
- 连接平台:创建一个托管链接,让所有者连接尚缺的平台。
SDK 与集成
- `sofia` CLI:发布在 npm 上、名为
sofia-post的 CLI,用来在终端中操控 Sofia。 - MCP 服务器:把 Sofia 接入你的 AI 智能体。
- Zapier:Sofia 集成使用你的个人密钥连接。它提供触发器「Post Published」和「Post Failed」、操作「Create Post」(发布、定时或保存草稿)以及搜索「Find Post」。它尚未提交到 Zapier 的公开目录。
身份验证
每次调用都在 Authorization 请求头中携带 Bearer 令牌。Sofia 每次调用都会重新读取与它关联的权限:被移出 Workspace 的人,或被撤销的授权,会让令牌从下一次请求起失效。
有两种身份验证方式:
| 方式 | 适用于 | 令牌 |
|---|---|---|
| 个人密钥 | 自动化你自己的账号、脚本、智能体 | sofia_pk_live_…,在到期前有效 |
| OAuth,授权码 + PKCE | 代表其用户操作的产品,作用于用户所选的 Workspace | sofia_live_at_…,一小时,可续期 |
| OAuth,client credentials | 你的某台服务器,用来自动化你作为所有者的 Workspace | sofia_live_at_…,一小时 |
API 密钥
如果你自动化的是自己的 Workspace,一个个人密钥就够了。在 Sofia 应用中,打开设置 › API,进入个人访问标签页,创建一个密钥。选择它的名称、有效期和权限。密钥只显示一次:请复制并保存在密钥保险库中,绝不要放进你的代码里。
在设置 › API › 个人访问中创建一个密钥。它以你的身份作用于你所有的 Workspace:你指定的商家决定 Workspace,你在其中的角色决定密钥能做什么。
把密钥放在 Authorization 请求头中,作为 Bearer 令牌发送:
curl https://api.sofia-post.com/api/v1/establishments \
-H "Authorization: Bearer sofia_pk_live_…"`GET /v1/me` 会告诉你密钥代表谁操作、它的 Workspace 和它的权限。
缺少所需权限(scope)的调用会返回 403 insufficient_scope。
OAuth2 令牌
如果是代表用户在其自己的 Workspace 上操作的 SaaS 产品,请注册一个 OAuth 客户端:每个人在自己选择的 Workspace 上授权你的应用。OAuth 访问令牌以 sofia_live_at_… 开头,有效期一小时,使用方式相同:
curl https://api.sofia-post.com/api/v1/establishments \
-H "Authorization: Bearer sofia_live_at_…"你要创建一个供其他 Sofia 账号连接的应用吗?它的 authorization code + PKCE 流程、client credentials 和 scope 在 OAuth2 中说明。
基础 URL
所有端点都位于同一个基础 URL 之下:
https://api.sofia-post.com/api/v1速率限制
Sofia API 在三个时间窗口内统计请求:15 分钟、24 小时和 30 天。
| 令牌 | 15 分钟 | 24 小时 | 30 天 |
|---|---|---|---|
| 个人密钥(按账号) | 150 | 1,000 | 10,000 |
| OAuth 客户端 | 100 | 500 | 15,000 |
| OAuth 客户端,按每位授权用户(最多 1,000 位) | +10 | +25 | +100 |
请求头
每个响应都会告诉你剩余额度,采用 IETF 格式:
RateLimit: "15m";r=144;t=43, "24h";r=994;t=13543, "30d";r=9994;t=13543
RateLimit-Policy: "15m";q=150;w=900, "24h";q=1000;w=86400, "30d";q=10000;w=2592000r 是剩余请求数,t 是窗口重置前的秒数,q 是配额,w 是窗口的秒数。
窗口用尽时
响应为 429 rate_limit_exceeded,带有 Retry-After 请求头,并在 details 中给出已用尽的窗口。请等待该时长后再重试。
高效使用 API
- 注册 Webhook,而不是循环轮询某条帖子。
- 使用最大为 100 的
limit读取列表,以减少页数。 - 缓存不常变化的数据:平台、商家、已连接账号。
错误
每个错误都会返回一个 HTTP 状态码和同样的 JSON 信封。code 是你的代码读取的约定;message 是写给人看的,可能会变。
{
"error": {
"code": "invalid_request",
"message": "The request is invalid",
"details": [
{ "field": "targets[0].text", "code": "required", "message": "targets[0].text is required" }
],
"docUrl": "https://www.sofia-post.com/developers/errors#invalid_request",
"requestId": "2aab5943-6333-4caf-8478-25a85ebd4abf"
}
}details提供更多信息:被拒绝的字段、某个平台的原因、已用尽的限制窗口。docUrl指向本页对该错误码的说明。requestId标识这次请求:请把它提供给支持团队。
错误码
| 状态码 | 错误码 | 含义 |
|---|---|---|
| 400 | invalid_request | 请求体、查询或某个参数无效。每个被拒绝的字段对应一条详情:field、code(required、is_string、max……)和 message。 |
| 400 | invalid_json | 请求体不是有效的 JSON。 |
| 400 | invalid_cursor | 该游标不是由 Sofia API 返回的。 |
| 400 | webhook_url_invalid | webhook 地址不是 https,或指向私有地址。 |
| 400 | webhook_event_invalid | 未知的 webhook 事件。 |
| 400 | application_name_invalid | 设置 › API:客户端名称缺失或过长。 |
| 400 | application_profile_invalid | 设置 › API:客户端资料中有字段无效;详情会指出是哪一个。 |
| 400 | logo_invalid | 设置 › API:徽标被拒绝(至少 180×180 px,PNG、JPEG 或 WebP,最大 2 MB)。 |
| 401 | invalid_token | 令牌缺失、未知、已过期或已被撤销,或令牌背后的用户已失去其所带的权限。参见 WWW-Authenticate 请求头。 |
| 401 | unauthorized | 需要身份验证。 |
| 402 | subscription_required | live 写入(包括试运行)需要有效的订阅或试用。 |
| 403 | insufficient_scope | 令牌缺少该端点所需的 scope;详情中会指出是哪一个。 |
| 403 | insufficient_role | 你在此 Workspace 中的角色不允许执行该操作。 |
| 403 | live_mode_required | 用测试令牌进行了真实写入。 |
| 403 | route_closed_to_applications | 该端点不对应用开放。 |
| 403 | forbidden | 访问被拒绝。 |
| 403 | session_required | 设置 › API:此操作需要 Sofia 会话,而不是令牌。 |
| 403 | impersonation_forbidden | 设置 › API:此操作在借用的会话中会被拒绝。 |
| 403 | workspace_owner_required | 只有 Workspace 的所有者可以执行此操作。 |
| 403 | workspace_member_required | 你不是此 Workspace 的成员。 |
| 404 | route_not_found | 没有端点响应该方法和路径。 |
| 404 | establishment_not_found | 未知的门店,或超出令牌的访问范围。 |
| 404 | media_not_found | 未知的媒体,或超出令牌的访问范围。 |
| 404 | post_not_found | 未知的帖子,或超出令牌的访问范围。 |
| 404 | webhook_not_found | 未知的 webhook,或属于另一个应用。 |
| 404 | not_found | 资源不存在。 |
| 404 | application_not_found | 设置 › API:未知的客户端。 |
| 404 | grant_not_found | 授权未知,或已被撤销。 |
| 404 | personal_key_not_found | 设置 › API:未知的个人密钥。 |
| 404 | consent_not_found | 授权请求已过期或不存在:请从授权端点重新开始。 |
| 405 | method_not_allowed | 此端点不允许使用该方法。 |
| 409 | post_in_flight | 有网络正在发送该帖子:请稍后重试删除。 |
| 409 | post_not_editable | 已有网络发送了该帖子:不能再编辑。 |
| 409 | account_already_connected | 该网络已连接到此门店。 |
| 409 | webhook_limit_reached | 该应用已有 10 个 webhook。 |
| 409 | idempotency_in_progress | 使用此 Idempotency-Key 的第一个请求仍在处理中。 |
| 409 | conflict | 请求与资源的当前状态冲突。 |
| 409 | scope_removal_forbidden | 设置 › API:客户端的权限可以添加,但不能移除。 |
| 409 | public_client_has_no_secret | 设置 › API:公开客户端没有密钥。 |
| 413 | payload_too_large | 文件超出上传限制。 |
| 415 | unsupported_media_type | 不支持的内容类型。 |
| 422 | post_refused | 某个网络、门店或你的套餐会拒绝该帖子。每个原因对应一条详情,各自带有 message。 |
| 422 | media_import_refused | 无法从该 URL 导入媒体;详情中给出了原因。 |
| 422 | media_upload_refused | 上传的文件不是受支持的图片或视频。 |
| 422 | platform_not_offered | 该网络不提供连接。 |
| 422 | idempotency_key_reused | 此 Idempotency-Key 曾用于另一个请求体。 |
| 422 | unprocessable_entity | 请求可以理解,但无法处理。 |
| 429 | rate_limit_exceeded | 速率限制窗口已满:请等待 Retry-After 指定的时间。 |
帖子被拒绝的原因
post_refused 会在 details[].code 中列出每个原因:
| 错误码 | 含义 |
|---|---|
unsupported_platform | 不支持该网络。 |
duplicate_platform | 同一个网络被指定了两次。 |
unsupported_format | 该网络不提供这种格式。 |
caption_too_long | 文本过长(max、actual)。 |
too_many_hashtags | 话题标签过多(max、actual)。 |
too_many_mentions | @ 提及过多(max、actual)。 |
too_many_thread_posts | 串文过长(max、actual)。 |
too_many_media | 媒体过多(max、actual)。 |
media_mix_refused | 该网络不允许混合照片和视频。 |
media_required | 该网络需要照片或视频。 |
media_type_refused | 该网络不接受这种类型的媒体。 |
scheduled_in_past | scheduledAt 在过去。 |
scheduled_at_invalid | 定时帖子需要未来的 scheduledAt。 |
account_not_connected | 此门店在该网络上没有已连接的账号。 |
account_ambiguous | 该网络上连接了多个账号:请用 accountId 指定一个。 |
media_not_found | 该媒体不在此门店的媒体库中。 |
media_url_invalid | 媒体 URL 必须是公开的 https 地址。 |
platform_unavailable | 该网络在 Sofia 上尚未开放。 |
publication_quota_exceeded | 已达到你套餐的发布配额。 |
处理错误
- 请根据
code来编写逻辑,不要依赖message。 - 遇到
429,在Retry-After之后重试;遇到5xx,使用指数退避重试。 - 不要原样重试
4xx:先修正请求。 - 每次失败都记录
requestId。
分页
列表(GET /v1/posts、GET /v1/media)按最新优先返回,一次一页。limit 设置每页大小,范围 1 到 100(默认 20)。
{
"data": [ … ],
"nextCursor": "WyIyMDI2LTEwLTI3VDE1OjExOjU5LjAxN1oiLCIx…"
}把 nextCursor 作为 cursor 传入即可读取下一页。当没有 nextCursor 时,说明已经到末尾。
let cursor;
do {
const url = new URL('https://api.sofia-post.com/api/v1/posts');
url.searchParams.set('limit', '100');
if (cursor) url.searchParams.set('cursor', cursor);
const page = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${key}` } }).then((r) => r.json());
handle(page.data);
cursor = page.nextCursor;
} while (cursor);游标是不透明的:切勿自行构造或修改。Sofia 没有返回过的游标会返回 400 invalid_cursor。
幂等性
网络错误可能让你无法确定帖子是否已创建。请在 POST /v1/posts 和 POST /v1/media 中发送 Idempotency-Key 请求头,并用同一个密钥重试:Sofia 会返回第一次的响应,不会重复创建任何内容。
curl -X POST https://api.sofia-post.com/api/v1/posts \
-H "Authorization: Bearer $SOFIA_API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d @post.json- 每条新帖子使用一个新的随机密钥(UUID),同一条帖子的每次重试使用同一个密钥。
- 同一个密钥配上不同的请求体,会返回
422 idempotency_key_reused。 - 第一次请求仍在处理时,同一个密钥会返回
409 idempotency_in_progress:请稍后重试。
测试模式与模拟
模拟。在 POST /v1/posts 后加上 ?dry_run=true:Sofia 会执行真实发布的各项检查(每个平台的规则、已连接账号、媒体、日期和你的套餐),但不会保存或发送任何内容。响应的详情见发布页面。
测试模式。以测试模式创建的应用(令牌 sofia_test_at_…)可以读取你真实的 Workspace,但绝不会写入:它的帖子是模拟的,并按同样的规则判定,任何真实写入都会返回 403 live_mode_required。在设置 › API 中创建的密钥和客户端是正式模式;如需在不发布的前提下检查,请使用模拟。
OAuth
/v1/oauth/authorize发起授权在用户的浏览器中打开。未知客户端或未注册的 redirect_uri 返回 400,否则跳转到 Sofia 授权页面。GET/.well-known/oauth-authorization-server/api/v1/oauth获取服务器元数据返回 Sofia API 的 OAuth 2.0 授权服务器元数据(RFC 8414),方便客户端发现端点和支持的选项。POST/v1/oauth/token签发访问令牌client_credentials、authorization_code、refresh_token 可换取 3600 秒有效的令牌。账号
平台
门店
/v1/establishments列出门店列出令牌所操作 Workspace 的门店。发布、列出媒体或读取数据洞察时,用其标识符作为 establishmentId。GET/v1/establishments/{id}/accounts列出已连接账号列出门店已连接的网络,不含令牌或网络密钥,只提供为账号命名并在发布时选作 accountId 所需的全部信息。POST/v1/establishments/{id}/connection-links创建连接链接返回托管链接,用户在 Sofia 上授权网络,因此网络令牌不会到达你的应用。链接有效期 30 分钟,仅限正式应用使用。GET/v1/establishments/{id}/insights获取门店数据洞察返回 Sofia 为每个已连接账号收集的最新有效数据。尚未收集到数据的账号显示 awaiting,绝不会显示为零。媒体
/v1/media列出媒体按从新到旧分页列出门店的媒体库,附当前时间窗口内有效的签名 URL。在测试模式和正式模式下都可以使用,用于挑选已有素材。POST/v1/media导入或上传媒体把文件(multipart)或 Sofia 抓取的 https URL 添加到门店媒体库。需要正式应用和有效订阅。GET/v1/media/{id}获取媒体返回媒体库中的一项媒体,附当前时间窗口内有效的签名 URL,其标识符可在发布帖子时直接复用,无需重新上传文件。DELETE/v1/media/{id}移除媒体像应用一样,把媒体从其所属门店的媒体库中移除。文件本身不会被销毁,已排期的帖子仍然保留它的视觉素材,不受影响。帖子
/v1/posts列出帖子按从新到旧列出令牌可访问门店的帖子,可按门店、状态、日期范围和作者筛选,分页返回,并用游标取得下一页,每页最多 100 条。POST/v1/posts发布帖子正式应用可立即发布、定时发布或存为草稿,测试应用只做模拟。加上 ?dry_run=true 则只检查、不写入。GET/v1/posts/{id}获取帖子返回帖子及各网络的状态、发布后的链接和失败时的错误。正式模式可读 Workspace 内任何帖子,测试模式读模拟帖子。PATCH/v1/posts/{id}编辑帖子任何网络开始发送前,可替换 targets、设置 scheduledAt,或在 draft 与 scheduled 之间切换,其余字段不变。DELETE/v1/posts/{id}删除帖子像应用一样删除帖子:已排期的不再发送,已发布的会从各网络上移除。测试应用没有任何真实内容,因此没有东西可删除。Webhook
/v1/webhooks列出 webhook列出应用的 webhook,不含签名密钥;该密钥只在注册 webhook 时返回一次,之后无法再次获取,请自行妥善保存。POST/v1/webhooks注册 webhook注册 Sofia 以带签名 JSON 事件调用的 https URL,失败最多重试五次。密钥仅显示一次,最多 10 个。DELETE/v1/webhooks/{id}删除 webhook删除 webhook。之后它不再收到任何内容,包括仍在等待重试的投递;调用成功后不返回响应体,删除后也无法恢复。POST/v1/webhooks/{id}/test发送测试事件像其他事件一样投递带签名的 webhook.test 事件,无需等待接收方,便于校验签名。每分钟最多发送五次。