概览API 概览

概览

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代表其用户操作的产品,作用于用户所选的 Workspacesofia_live_at_…,一小时,可续期
OAuth,client credentials你的某台服务器,用来自动化你作为所有者的 Workspacesofia_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 天
个人密钥(按账号)1501,00010,000
OAuth 客户端10050015,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=2592000

r 是剩余请求数,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 标识这次请求:请把它提供给支持团队。

错误码

状态码错误码含义
400invalid_request请求体、查询或某个参数无效。每个被拒绝的字段对应一条详情:field、code(required、is_string、max……)和 message。
400invalid_json请求体不是有效的 JSON。
400invalid_cursor该游标不是由 Sofia API 返回的。
400webhook_url_invalidwebhook 地址不是 https,或指向私有地址。
400webhook_event_invalid未知的 webhook 事件。
400application_name_invalid设置 › API:客户端名称缺失或过长。
400application_profile_invalid设置 › API:客户端资料中有字段无效;详情会指出是哪一个。
400logo_invalid设置 › API:徽标被拒绝(至少 180×180 px,PNG、JPEG 或 WebP,最大 2 MB)。
401invalid_token令牌缺失、未知、已过期或已被撤销,或令牌背后的用户已失去其所带的权限。参见 WWW-Authenticate 请求头。
401unauthorized需要身份验证。
402subscription_requiredlive 写入(包括试运行)需要有效的订阅或试用。
403insufficient_scope令牌缺少该端点所需的 scope;详情中会指出是哪一个。
403insufficient_role你在此 Workspace 中的角色不允许执行该操作。
403live_mode_required用测试令牌进行了真实写入。
403route_closed_to_applications该端点不对应用开放。
403forbidden访问被拒绝。
403session_required设置 › API:此操作需要 Sofia 会话,而不是令牌。
403impersonation_forbidden设置 › API:此操作在借用的会话中会被拒绝。
403workspace_owner_required只有 Workspace 的所有者可以执行此操作。
403workspace_member_required你不是此 Workspace 的成员。
404route_not_found没有端点响应该方法和路径。
404establishment_not_found未知的门店,或超出令牌的访问范围。
404media_not_found未知的媒体,或超出令牌的访问范围。
404post_not_found未知的帖子,或超出令牌的访问范围。
404webhook_not_found未知的 webhook,或属于另一个应用。
404not_found资源不存在。
404application_not_found设置 › API:未知的客户端。
404grant_not_found授权未知,或已被撤销。
404personal_key_not_found设置 › API:未知的个人密钥。
405method_not_allowed此端点不允许使用该方法。
409post_in_flight有网络正在发送该帖子:请稍后重试删除。
409post_not_editable已有网络发送了该帖子:不能再编辑。
409account_already_connected该网络已连接到此门店。
409webhook_limit_reached该应用已有 10 个 webhook。
409idempotency_in_progress使用此 Idempotency-Key 的第一个请求仍在处理中。
409conflict请求与资源的当前状态冲突。
409scope_removal_forbidden设置 › API:客户端的权限可以添加,但不能移除。
409public_client_has_no_secret设置 › API:公开客户端没有密钥。
413payload_too_large文件超出上传限制。
415unsupported_media_type不支持的内容类型。
422post_refused某个网络、门店或你的套餐会拒绝该帖子。每个原因对应一条详情,各自带有 message。
422media_import_refused无法从该 URL 导入媒体;详情中给出了原因。
422media_upload_refused上传的文件不是受支持的图片或视频。
422platform_not_offered该网络不提供连接。
422idempotency_key_reused此 Idempotency-Key 曾用于另一个请求体。
422unprocessable_entity请求可以理解,但无法处理。
429rate_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_pastscheduledAt 在过去。
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

账号

平台

门店

媒体

帖子

Webhook