对外 API 接入文档

给渠道方与合作方:三个 SKU 的下单、查单与交付物获取。

1. 基础信息

Base URLhttp://127.0.0.1:8080
协议HTTP/1.1,Content-Type: application/json; charset=utf-8
编码请求与响应体一律 UTF-8;中文不转义
金额单位整数分,字段名以 _cents 结尾(3900 = 39.00 元)
时间格式ISO 8601 带时区偏移,例 2026-11-08T09:30:00+08:00;不带偏移的串会被拒绝
追踪请求可带 X-Request-Id;响应必回 meta.request_id
金额口径。API 层所有金额字段一律整数分并以 _cents 结尾,禁止浮点、禁止字符串元。 例:39 元 = 3900;599 元 = 59900;3999 元 = 399900。

2. 认证方式

三类凭证,互不通用。服务端到服务端用 API Key;客户自助查单用订单 Token。

2.1 API Key(渠道方 / 合作方)

两种写法等价,任选其一。密钥格式 sfk_<32 位 urlsafe base64>。

# 写法 A
Authorization: Bearer sfk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# 写法 B
X-API-Key: sfk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
存储服务端 data/api_keys.csv
比对方式常数时间比较(hmac.compare_digest),防时序侧信道
失效表中「启用」列非启用即拒绝;可随时置为禁用以吊销
错误缺失 → AUTH_TOKEN_MISSING(401);无效/禁用 → AUTH_TOKEN_INVALID(401)
日志凭证不得写入日志,只记录指纹

2.2 订单 Token(客户自助查单 / 下载)

X-Order-Token: <token>

下单成功时服务端签发,token 绑定到具体订单号。持 token 只能访问该订单,不能越权看别人的单。

2.3 公开路径(无需凭证)

精确匹配/api/health /api/skus /api/quote /
前缀匹配/api/skus/<sku>
下单接口默认需要 API Key。POST /api/order 在未提供凭证时返回 401; 仅当服务端设置环境变量 SF_ALLOW_PUBLIC_ORDER=1 时才允许匿名下单(该开关默认关闭)。

3. 统一响应格式

3.1 成功

{
  "ok": true,
  "data": { "...": "业务载荷" }
}

3.2 失败

{
  "ok": false,
  "error": {
    "code": "MISSING_REQUIRED_FIELD",
    "message": "缺少必填字段:name",
    "details": { "field": "name" },
    "retryable": false
  }
}
字段说明
ok布尔。成功恒为 true,失败恒为 false —— 先看这个
data成功时的业务载荷。删除类操作可为 null
error.code业务错误码。按 code 分支,不要解析 message
error.message人类可读说明,可直接展示给客户;文案不保证稳定
error.details字段级明细,便于定位到具体字段
error.retryable布尔。true = 退避后可安全重试;false = 重试无意义,须先改请求或状态
解析建议。先判 ok;失败时按 error.code 分支, error.retryable === true 才自动重试(并遵守 Retry-After)。 不要用 HTTP 状态码替代 error.code 做业务分支。

4. 端点全表

幂等列:是 = 副作用接口,必须带 Idempotency-Key;否 = 只读。

方法路径认证幂等说明
GET/api/health公开否健康检查:可用 SKU 与服务器时间
GET/api/skus公开否SKU 目录:规格、档位价格、交付周期、边界
GET/api/skus/<sku>公开否单个 SKU 详情
POST/api/quote公开否报价:按 SKU + 规格算档位与价格
POST/api/orderAPI Key是建单:写台账,返回订单号与价格
POST/api/previewAPI Key否预览:007 出图预览;016/033 返回文本摘要
GET/api/ordersAPI Key否订单列表
GET/api/portal/order订单 Token否客户自助查单:状态、进度、交付物、边界、返工规则
GET/api/portal/download/<order_id>/<file>订单 Token否下载交付物
GET/api/portal/status公开否服务状态:各 SKU 健康度与最近故障
GET/deliveries/<path>API Key否交付物目录下载

4.1 建单参数

字段类型必填说明
skustring是SKU 编号,007 / 016 / 033
specobject是规格对象,字段随 SKU 变化(见 GET /api/skus 的 required_inputs)
namestring是客户称呼,不能为空
phonestring是11 位手机号,必须匹配 ^\d{11}$
notestring否备注,默认空串
channelstring否来源渠道,默认 web

spec 各 SKU 的字段:

SKUspec 字段
007sizes[](id/one/two/passport)、backgrounds[](white/blue/red)、color(bool)、image_base64
016brand、product、audience、tone、platform(douyin/xiaohongshu/shipinhao/bilibili/other)、count(1–5)、seconds
033title、activity_type(general/workshop/meetup/exam)、time_place、host、quota、need_review(bool)

4.2 建单响应

{
  "ok": true,
  "data": {
    "order_id": "ORD-20261108-1A2B",
    "sku": "016",
    "tier": "script_pack",
    "tier_label": "脚本包",
    "price": 329,
    "includes": ["脚本正文", "字幕文件", "拍摄清单"],
    "not_in_scope": ["不含视频成片", "不含代运营发布"],
    "pay_note": "转账请备注订单号 ORD-20261108-1A2B",
    "delivery_hours": 48,
    "rework_free_limit": 2
  }
}
返工上限是 2 次/单。仅限「与已确认方向不符」的交付缺陷;客户改需求不免费。详见 GET /api/portal/order 返回的返工规则。

5. 幂等用法

所有产生副作用的请求(建单、退款、返工等)都必须带 Idempotency-Key。

Idempotency-Key: cust_9f2:order:1

# 也接受放在请求体里(字段名 idempotency_key),此时优先级低于请求头
{ "idempotency_key": "cust_9f2:order:1", "...": "..." }
生成方调用方生成,建议 UUID v4 或 ULID
命名建议{actor}:{op}:{业务实体ID}:{客户端自增序号}
命中行为返回首次成功响应,状态码与响应体完全一致,响应头带 X-Idempotent-Replay: true
保留期24 小时(TTL),过期记录自动清理
缺失副作用接口未带该头 → 400 IDEMPOTENCY_KEY_REQUIRED
同键不同体→ 409 IDEMPOTENCY_CONFLICT,须换新键或核对原请求
同键处理中→ 409 IDEMPOTENT_REQUEST_IN_PROGRESS,2 秒后以同键重试
并发保证同一 key 的并发请求只有一个拿到执行权,其余要么重放要么拿 409
重试时请用同一个 key。换 key 重试会创建重复订单 —— 这是幂等键存在的唯一意义。 网络超时、5xx 时不要换键重发。

6. 限流与 Retry-After

滑动窗口(sliding window)计数,窗口 60 秒,非固定窗口。超限返回 429。

端点配额说明
/api/order*10 次/分钟下单是低频高价值动作,配额更严
其它端点60 次/分钟默认配额
配额按调用方分桶。同一窗口内累计命中次数,超限即拒。窗口内最早的命中过期后即可重试。

6.1 响应头

Retry-After: 42
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1762562295
超限响应429error.code = RATE_LIMITED
退避策略按 Retry-After 秒数等待后重试;不要立即重试,也不要自己算退避窗口
自动重试服务端不自动重试;error.retryable 为 true 时才允许客户端退避重试
error.details限流时含 retry_after(秒)、limit、window_seconds、bucket

7. 错误码表

共 69 条,覆盖 400 / 401 / 403 / 404 / 409 / 422 / 429 / 500 / 503。

7.1 400 参数与请求格式

code含义客户端处理可重试
INVALID_ARGUMENT请求体或查询参数不符合规范按 details 修正字段后重发否
MISSING_REQUIRED_FIELD缺少必填字段补齐 details 中的字段否
INVALID_TIMESTAMP时间串格式或时区不合法改用 ISO 8601 带偏移格式否
INVALID_AMOUNT_FORMAT金额非整数分或超出范围改为 _cents 整数否
INVALID_CURSOR分页游标非法或已过期重新从首页查询否
INVALID_ENUM_VALUE枚举值不在允许集合按 details 中的 allowed 修正否
IDEMPOTENCY_KEY_REQUIRED副作用接口缺少幂等键补齐后重发否
IDEMPOTENCY_KEY_CONFLICT同幂等键对应不同请求体(契约码)换新键或核对原请求否
UNSUPPORTED_MEDIA_TYPEContent-Type 不支持改用 application/json否
PAYLOAD_TOO_LARGE请求体或文件超过限制改用预签名直传否

7.2 401 / 403 认证与授权

codeHTTP含义可重试
AUTH_TOKEN_MISSING401未携带凭证否
AUTH_TOKEN_INVALID401凭证无效或已吊销否
AUTH_TOKEN_EXPIRED401令牌已过期否
AUTH_CREDENTIALS_INVALID401账号或密码错误否
AUTH_CSRF_INVALID403管理端 CSRF 校验失败否
AUTH_IP_NOT_ALLOWED403来源 IP 不在白名单否
PERMISSION_DENIED403已认证但无该操作权限否
RESOURCE_NOT_OWNED403资源不属于当前主体否
SUBSCRIPTION_FEATURE_LOCKED403当前订阅档位不含该能力否
COMPLIANCE_ACCOUNT_RESTRICTED403账户因合规复核被限制否

7.3 404 资源不存在

code含义
SKU_NOT_FOUNDSKU 不存在或已下架
ORDER_NOT_FOUND订单不存在
WORKCART_NOT_FOUND作业卡不存在
SUBSCRIPTION_NOT_FOUND订阅不存在
PAYMENT_NOT_FOUND支付记录不存在
ARTIFACT_NOT_FOUND交付物不存在
UPLOAD_NOT_FOUND上传会话不存在或已过期
PRODUCT_NOT_FOUND工作台产品不存在
ENDPOINT_NOT_FOUND路径不存在
METHOD_NOT_ALLOWED路径存在但方法不支持

7.4 409 冲突

code含义可重试
RESOURCE_VERSION_CONFLICT乐观锁版本号不一致是
STATE_TRANSITION_NOT_ALLOWED当前状态不允许该迁移否
IDEMPOTENCY_CONFLICT同幂等键对应不同请求体(网关实际返回)否
IDEMPOTENT_REQUEST_IN_PROGRESS同幂等键请求处理中(2 秒后同键重试)是
QUOTE_EXPIRED报价已过期否
DUPLICATE_ORDER_IN_PROGRESS同一客户与 SKU 有进行中订单否
PAYMENT_AMOUNT_MISMATCH回调金额与订单应付不符否
REFUND_ALREADY_PROCESSED该笔已退款否
WORKCART_ALREADY_DELIVERED作业卡已交付否
SUBSCRIPTION_ALREADY_CANCELED订阅已取消否
PRICE_CHANGE_REQUIRES_SECOND_APPROVAL改价需第二审批角色否

7.5 422 语义校验失败

code含义可重试
VALIDATION_FAILED字段级业务校验失败否
REQUIREMENTS_INQUIRY_INCOMPLETE需求五问未答全否
QUOTE_TIER_NOT_ALLOWEDSKU 不支持所选档位否
PRICE_BAND_VIOLATION价格超出该档位允许区间否
CONSIGNMENT_WINDOW_EXCEEDED交付窗口超出 SKU 承诺周期否
WORKCART_STEP_PRECONDITION_FAILED步骤提交前置条件不满足否
REWORK_RULE_VIOLATION返工请求超出作业卡返工规则否
REWORK_LIMIT_EXCEEDED单笔返工次数已达上限否
COMPLIANCE_RECORD_MISSING缺少合规记录(授权书/监护人同意/AI 标识)否
CONSENT_RECORD_INVALID授权书或同意书缺签署人/日期/授权范围否
MINOR_SUBJECT_UNCONFIRMED素材含疑似未成年人但无监护人同意否
AI_DISCLOSURE_ACKNOWLEDGEMENT_MISSING未确认 AI 生成内容标识声明否
COMPLIANCE_REVIEW_REQUIRED该类目需合规审查后推进否
PARTNER_QUALIFICATION_MISSING方案档缺少合作方资质否
PROMISE_FORBIDDEN_CLAIM文案含禁止承诺否
REDLINE_POLICY_VIOLATION触碰四条红线,接口直接拒绝否
QUOTA_EXCEEDED账户配额用尽否
MATERIAL_RIGHTS_UNCONFIRMED素材权属未确认否

7.6 429 限流与配额 / 5xx 服务端

codeHTTP含义可重试
RATE_LIMITED429速率超限(网关实际返回,带 Retry-After)是
RATE_LIMIT_EXCEEDED429速率超限(契约正式码)是
MODEL_QUOTA_EXCEEDED429模型调用配额用尽是
CONCURRENT_EXECUTION_LIMIT429同一作业卡并发执行超限是
ORDER_QUOTA_EXCEEDED429账户同时进行中订单数超限否
INTERNAL_ERROR500未归类服务端异常是
AGENT_EXECUTION_FAILED500数字员工执行失败是
PIPELINE_TIMEOUT500流水线执行超时是
ARTIFACT_GENERATION_FAILED500成品生成失败是
DATA_INTEGRITY_ERROR500数据一致性校验失败否
SERVICE_UNAVAILABLE503服务暂时不可用是
UPSTREAM_TIMEOUT503依赖的模型/支付/对象存储超时是

7.7 别名与「契约码 vs 实际返回码」

网关内部有少量集成别名,与 14 号契约的正式码一一对应。对外返回哪个码,以「实际返回码」列为准;两列都列出来,是为了让你在日志与文档之间不会对不上。

实际返回码契约正式码HTTP说明
UNAUTHORIZEDAUTH_TOKEN_INVALID401凭证无效或已吊销
IDEMPOTENCY_CONFLICTIDEMPOTENCY_KEY_CONFLICT409同幂等键不同请求体
RATE_LIMITEDRATE_LIMIT_EXCEEDED429触发限流
两种码都要能处理。建议在客户端把上表右列的契约码与左列的实际码都当作同一语义处理 (比如 RATE_LIMITED 与 RATE_LIMIT_EXCEEDED 走同一套退避逻辑), 避免网关调整后客户端突然失配。

8. 调用示例

8.1 curl

BASE=http://127.0.0.1:8080
KEY=sfk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# 1. 看 SKU 目录(公开)
curl -s $BASE/api/skus | head -c 400

# 2. 报价(公开)
curl -s -X POST $BASE/api/quote \
  -H 'Content-Type: application/json' \
  -d '{"sku":"016","spec":{"brand":"青山茶铺","product":"冷萃乌龙茶",
       "audience":"一二线城市白领","platform":"douyin","count":5}}'

# 3. 建单(需 API Key + 幂等键)
curl -s -X POST $BASE/api/order \
  -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: channel_a:order:1' \
  -d '{"sku":"016","spec":{"brand":"青山茶铺","product":"冷萃乌龙茶",
       "audience":"一二线城市白领","platform":"douyin","count":5},
       "name":"青山茶铺","phone":"13812345678","channel":"xhs"}'

# 4. 客户查单(订单 Token)
curl -s "$BASE/api/portal/order?order_id=ORD-20261108-1A2B" \
  -H "X-Order-Token: $TOKEN"

# 5. 下载交付物
curl -s -o out.zip "$BASE/api/portal/download/ORD-20261108-1A2B/脚本包.zip" \
  -H "X-Order-Token: $TOKEN"

8.2 Python

import os, time, uuid, json, urllib.request, urllib.error

BASE = os.environ.get("SF_BASE", "http://127.0.0.1:8080")
KEY  = os.environ.get("SF_API_KEY", "")

def call(method, path, body=None, headers=None, idem=None):
    h = {"Accept": "application/json"}
    if body is not None:
        h["Content-Type"] = "application/json"
    if KEY:
        h["Authorization"] = "Bearer " + KEY
    if idem:
        h["Idempotency-Key"] = idem
    h.update(headers or {})

    data = json.dumps(body).encode("utf-8") if body is not None else None
    req = urllib.request.Request(BASE + path, data=data, headers=h, method=method)

    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return r.status, json.loads(r.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        payload = json.loads(e.read().decode("utf-8") or "{}")
        return e.code, payload

def is_retryable(payload):
    return bool(payload.get("error", {}).get("retryable"))

def create_order(sku, spec, name, phone, channel="web"):
    # 幂等键在整个重试过程中保持不变
    key = "channel_a:order:" + str(uuid.uuid4())
    for attempt in range(3):
        status, payload = call("POST", "/api/order",
                               {"sku": sku, "spec": spec, "name": name,
                                "phone": phone, "channel": channel}, idem=key)
        if status < 300:
            return payload["data"]
        if not is_retryable(payload):
            raise RuntimeError(payload["error"])
        time.sleep(2 ** attempt)   # 退避
    raise RuntimeError("重试 3 次仍失败")

data = create_order(
    "016",
    {"brand": "青山茶铺", "product": "冷萃乌龙茶",
     "audience": "一二线城市白领", "platform": "douyin", "count": 5},
    "青山茶铺", "13812345678", channel="xhs",
)
print("订单号:", data["order_id"], "金额:", data["price"], "元")
print("付款备注:", data["pay_note"])

8.3 JavaScript

const BASE = "http://127.0.0.1:8080";
const KEY = "sfk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

async function call(method, path, body, { idempotencyKey, headers = {} } = {}) {
  const h = { Accept: "application/json", ...headers };
  if (KEY) h["Authorization"] = `Bearer ${KEY}`;
  if (body !== undefined) h["Content-Type"] = "application/json";
  if (idempotencyKey) h["Idempotency-Key"] = idempotencyKey;

  const res = await fetch(BASE + path, {
    method,
    headers: h,
    body: body === undefined ? undefined : JSON.stringify(body),
  });

  const payload = await res.json().catch(() => ({}));
  if (!payload.ok) {
    const err = new Error(payload.error?.message || "请求失败");
    err.code = payload.error?.code;
    err.retryable = !!payload.error?.retryable;
    err.status = res.status;
    if (res.status === 429) err.retryAfter = Number(res.headers.get("Retry-After") || 5);
    throw err;
  }
  return payload.data;
}

async function createOrder(sku, spec, name, phone, channel = "web") {
  const key = `channel_a:order:${crypto.randomUUID()}`;
  for (let i = 0; i < 3; i++) {
    try {
      return await call("POST", "/api/order",
        { sku, spec, name, phone, channel }, { idempotencyKey: key });
    } catch (e) {
      if (!e.retryable) throw e;             // 只重试可重试错误
      const wait = e.retryAfter ?? 2 ** i;    // 限流按 Retry-After
      await new Promise(r => setTimeout(r, wait * 1000));
    }
  }
  throw new Error("重试 3 次仍失败");
}

const data = await createOrder("016", {
  brand: "青山茶铺", product: "冷萃乌龙茶",
  audience: "一二线城市白领", platform: "douyin", count: 5,
}, "青山茶铺", "13812345678", "xhs");

console.log("订单号:", data.order_id, "金额:", data.price, "元");
console.log("付款备注:", data.pay_note);

9. 接入检查清单

  • 已拿到 API Key,且在服务端环境变量里保存(不要写进前端代码)
  • 所有副作用请求都带 Idempotency-Key,且重试时沿用同一个 key
  • 先判 ok 再读 data;失败按 error.code 分支
  • 只对 error.retryable === true 的错误做退避重试,且尊重 Retry-After
  • 金额按整数分处理,字段名用 _cents,不做浮点运算
  • 时间用 ISO 8601 带偏移格式
  • 下单成功后把 order_id 与 pay_note 一起展示给客户
  • 把 X-Order-Token 交给客户用于查单与下载,不经其他渠道外泄
  • 不承诺播放量、涨粉、成功率等结果(接口会拒绝违规承诺文案)
  • 人像类素材先取得授权书;未成年人须有监护人书面同意
  • AI 生成内容的标识不裁剪、不去除
  • 错误提示展示 error.message,但分支逻辑只用 error.code
  • 已用 X-Request-Id 串联调用链,便于排查