sfappAPI 文件

快速開始

這一輪跑完查身分、建草稿、讀回文章、刪除草稿。每一步都附 curl、Python 與 JavaScript 範例。

Python 範例需要 requests 套件,先執行 pip install requests。JavaScript 範例存成副檔名 .mjs 的檔案,用 Node 18 以上執行。

設定環境變數

把金鑰放進環境變數,後面每個範例都從這裡讀。

export SFAPP_KEY='sfapp_live_ab12cd34_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
export SFAPP_BASE='https://api.sfec.cloud'
import os

api_key = os.environ["SFAPP_KEY"]
base_url = "https://api.sfec.cloud"
print("金鑰前綴:", api_key[:19])
const apiKey = process.env.SFAPP_KEY;
const baseUrl = "https://api.sfec.cloud";
console.log("金鑰前綴:", apiKey.slice(0, 19));

金鑰格式與保管方式見認證與金鑰

查身分

GET /v1/me 回傳商店名稱、這把金鑰的權限、功能開關、欄位上限與今日用量。

curl -sS "$SFAPP_BASE/v1/me" \
  -H "Authorization: Bearer $SFAPP_KEY"
import os

import requests

BASE_URL = "https://api.sfec.cloud"

response = requests.get(
    f"{BASE_URL}/v1/me",
    headers={"Authorization": f"Bearer {os.environ['SFAPP_KEY']}"},
    timeout=30,
)
response.raise_for_status()
me = response.json()

print("商店:", me["shop_name"])
print("權限:", me["scopes"])
print("功能:", me["features"])
print("今日文章:", me["quotas"]["posts_created_today"], "/", me["quotas"]["posts_per_day"])
const baseUrl = "https://api.sfec.cloud";

const response = await fetch(`${baseUrl}/v1/me`, {
  headers: { Authorization: `Bearer ${process.env.SFAPP_KEY}` },
});
const me = await response.json();

console.log("商店:", me.shop_name);
console.log("權限:", me.scopes);
console.log("功能:", me.features);
console.log("今日文章:", me.quotas.posts_created_today, "/", me.quotas.posts_per_day);

回應節錄:

{
  "app_id": "app_7f3a9c1e",
  "app_name": "內容代理程式",
  "shop_name": "範例商店",
  "shop_public_url": "https://www.example-shop.tw",
  "scopes": ["read_blog", "write_blog", "read_audit"],
  "features": {
    "blog": true,
    "blog_categories": false,
    "media_upload": true,
    "summary": true,
    "cover_image": true,
    "published_at_sort": true
  },
  "quotas": {
    "posts_created_today": 3,
    "posts_per_day": 50
  },
  "api_version": "2026-09-14",
  "key_expires_at": "2027-09-14T00:00:00+08:00"
}

scopes 是這把金鑰能做的事。featuresfalse 的功能,相關端點回 404。limitsquotas 的逐鍵說明見限制與配額

建立草稿

POST /v1/blog/posts 建出來的文章狀態是 draft,顧客看不到。這支端點要帶 Idempotency-Key:新建用一把新的鍵,重試沿用同一把,規則見請求慣例

curl -sS -X POST "$SFAPP_BASE/v1/blog/posts" \
  -H "Authorization: Bearer $SFAPP_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "title": "秋季新品上架",
    "body_html": "<p>今年秋天的第一波新品已經到店。</p>",
    "summary": "秋季新品搶先看"
  }'
import os
import uuid

import requests

BASE_URL = "https://api.sfec.cloud"

response = requests.post(
    f"{BASE_URL}/v1/blog/posts",
    headers={
        "Authorization": f"Bearer {os.environ['SFAPP_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "title": "秋季新品上架",
        "body_html": "<p>今年秋天的第一波新品已經到店。</p>",
        "summary": "秋季新品搶先看",
    },
    timeout=30,
)
response.raise_for_status()
post = response.json()

print("文章識別碼:", post["id"], "狀態:", post["status"])
print("淨化提醒:", post.get("warnings", []))
import { randomUUID } from "node:crypto";

const baseUrl = "https://api.sfec.cloud";

const response = await fetch(`${baseUrl}/v1/blog/posts`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SFAPP_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": randomUUID(),
  },
  body: JSON.stringify({
    title: "秋季新品上架",
    body_html: "<p>今年秋天的第一波新品已經到店。</p>",
    summary: "秋季新品搶先看",
  }),
});
const post = await response.json();

console.log("文章識別碼:", post.id, "狀態:", post.status);
console.log("淨化提醒:", post.warnings ?? []);

成功回 201 與整篇文章。statusdraftpublished_atnull。內文依白名單淨化,被移除的項目列在 warnings

讀回文章

用上一步的識別碼讀回整篇,確認標題、摘要與淨化後的內文。

curl -sS "$SFAPP_BASE/v1/blog/posts/1001" \
  -H "Authorization: Bearer $SFAPP_KEY"
import os

import requests

BASE_URL = "https://api.sfec.cloud"
POST_ID = 1001

response = requests.get(
    f"{BASE_URL}/v1/blog/posts/{POST_ID}",
    headers={"Authorization": f"Bearer {os.environ['SFAPP_KEY']}"},
    timeout=30,
)
response.raise_for_status()
post = response.json()

print(post["title"], post["status"], post["public_url"])
print(post["body_html"])
const baseUrl = "https://api.sfec.cloud";
const postId = 1001;

const response = await fetch(`${baseUrl}/v1/blog/posts/${postId}`, {
  headers: { Authorization: `Bearer ${process.env.SFAPP_KEY}` },
});
const post = await response.json();

console.log(post.title, post.status, post.public_url);
console.log(post.body_html);

草稿的 public_urlnull,發布後才有前台網址。發布流程見發布文章

刪除草稿

刪除分兩次送出,完整流程見 文章。已發布或已排程的文章要先下架。

# 第一次:取得確認碼,回應為 409
curl -sS -X DELETE "$SFAPP_BASE/v1/blog/posts/1001" \
  -H "Authorization: Bearer $SFAPP_KEY"

# 第二次:填入上一步的 confirmation_token
curl -sS -i -X DELETE "$SFAPP_BASE/v1/blog/posts/1001" \
  -H "Authorization: Bearer $SFAPP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"confirmation_token": "貼上第一次拿到的確認碼"}'
import os

import requests

BASE_URL = "https://api.sfec.cloud"
POST_ID = 1001
HEADERS = {"Authorization": f"Bearer {os.environ['SFAPP_KEY']}"}


def delete_post(body: dict | None) -> tuple[int, dict | None]:
    """送出刪除請求,回傳狀態碼與回應內容;204 沒有內容。"""
    response = requests.delete(
        f"{BASE_URL}/v1/blog/posts/{POST_ID}",
        headers=HEADERS,
        json=body,
        timeout=30,
    )
    return response.status_code, None if response.status_code == 204 else response.json()


status, body = delete_post(None)
if status != 409:
    raise SystemExit(f"第一次應回 409,實際為 {status}:{body}")

details = {item["field"]: item["issue"] for item in body["error"]["details"]}
print("即將刪除:", details["post.title"])
print("確認碼有效秒數:", details["expires_in_seconds"])

# 人工確認後再送第二次
status, body = delete_post({"confirmation_token": details["confirmation_token"]})
print("刪除結果:", status)
const baseUrl = "https://api.sfec.cloud";
const postId = 1001;
const headers = {
  Authorization: `Bearer ${process.env.SFAPP_KEY}`,
  "Content-Type": "application/json",
};

async function deletePost(body) {
  const response = await fetch(`${baseUrl}/v1/blog/posts/${postId}`, {
    method: "DELETE",
    headers,
    body: body === null ? undefined : JSON.stringify(body),
  });
  return { status: response.status, body: response.status === 204 ? null : await response.json() };
}

const first = await deletePost(null);
if (first.status !== 409) {
  throw new Error(`第一次應回 409,實際為 ${first.status}`);
}

const details = Object.fromEntries(
  first.body.error.details.map((item) => [item.field, item.issue]),
);
console.log("即將刪除:", details["post.title"]);
console.log("確認碼有效秒數:", details["expires_in_seconds"]);

// 人工確認後再送第二次
const second = await deletePost({ confirmation_token: details["confirmation_token"] });
console.log("刪除結果:", second.status);

一行跑完

這一行依序查身分、建一篇草稿、讀回文章,並印出每步結果。腳本只讀取與建立草稿,不發布、不刪除、不上傳圖片,只依賴 curlpython3。跑完會留下一篇標題開頭為 [Quickstart] 的草稿,照上一節刪掉即可。

curl -fsSL https://api.sfec.cloud/quickstart.sh | SFAPP_KEY="$SFAPP_KEY" bash