概覽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 的人,或被撤銷的授權,會讓權杖從下一次請求起失效。

有兩種身分驗證方式:

方式適用於權杖
個人金鑰自動化你自己的帳號、指令碼、AI 代理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