Yeti-Switch REST API 使用详解

Yeti-Switch 双 REST API 详解:Admin API(JSONAPI 规范 + JWT 认证 + 全实体 CRUD:Accounts/Balances/Gateways/Rateplans/Destinations/RoutingPlans/ActiveCalls/断呼)与 Customer API(客户门户后端),附 curl 实测 + Python SDK 封装 + 生产集成建议。

Yeti-SwitchYeti-SwitchSIP 协议VoIP 语音SBC电信软交换REST2026/10/27

Yeti-Switch REST API 使用详解

目标:完整覆盖 Yeti-Switch v1.14 暴露的两套 REST API——Admin API(管理平面,JSONAPI 规范,JWT 认证,全实体 CRUD)与 Customer API(客户门户后端,JSONAPI 规范,JWT 认证,CDR 只读),附 curl 实测命令、响应结构、错误码与生产集成建议。所有端点来自 demo.yeti-switch.org 自动生成文档(检索日期 2026-10-27),demo 管理员账号 admin / 111111。

一、环境信息

  • 目标 API 版本:Yeti-Switch v1.14(Admin API + Customer API)
  • 基础 URL:Admin https://demo.yeti-switch.org/api/rest/admin,Customer https://demo.yeti-switch.org/api/rest/customer/v1
  • 协议规范:JSONAPI 1.0(https://jsonapi.org/)
  • 认证:Admin API → POST /auth 获取 JWT;Customer API → 同机制但 auth[login] + auth[password] 走 Customer Portal 登录凭据
  • Demo 凭据:Admin admin / 111111,Customer Portal 需 yeti-web 创建 Customer Portal Login 后获取
  • 客户端工具:curl(实测)、yetio(yetisoftware 官方 Python SDK)、yeti-switch-api PyPI 包
  • API 日志位置:yeti-web Web UI → Logs → API Log(默认不记录 payload,需在 System → API Logs Config 开启 Debug)

二、前置条件

2.1 Admin API 访问控制

Admin API 默认被官方 Nginx 配置 yeti-web.dist.nginx 屏蔽(deny all;)。生产环境必须在 Nginx 中显式 allow 管理网段:

location /api/rest/admin/ {
    allow 10.0.0.0/8;      # 管理网段
    deny all;
    proxy_pass http://unix:/run/yeti/yeti-unicorn.sock;
}

2.2 JSONAPI 规范要点

所有端点统一遵循 https://jsonapi.org/ 规范:

  • 请求 Content-Type: application/vnd.api+json
  • 响应 Content-Type: application/vnd.api+json; charset=utf-8
  • 请求体格式:
    { "data": { "type": "accounts", "attributes": { ... } } }
    
  • 响应体格式:
    { "data": { "type": "...", "id": "...", "attributes": {...} },
      "links": { "self": "..." } }
    
  • 错误响应:
    { "errors": [{ "title": "...", "detail": "...", "status": "404" }] }
    

2.3 客户端通用头

curl -s \
  -H "Accept: application/vnd.api+json" \
  -H "Content-Type: application/vnd.api+json" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "X-Request-Id: $(uuidgen)" \
  https://demo.yeti-switch.org/api/rest/admin/accounts

三、API 总览

3.1 Admin API 端点域(按功能分组)

下列为 1.14 demo 系统完整端点列表(自动生成),标注常用度:⭐=高频 / 中=常规 / 低=低频

端点域type常用方法说明
Authentication—POST /auth获取 JWT(唯一需要 username/password 的端点)
Accountsaccounts⭐ CRUD客户账户(计费实体)
ActiveCallsactivecalls⭐ GET / DELETE实时活动呼叫(可远程断呼)
ApiAccessesapiaccessesCRUDCustomer Portal 登录凭据
Balancesbalances⭐ PATCH账户余额设置(以 external_id 为主键)
BalancesCorrectionbalances_correctionPATCH余额加减 correction
CdrExportscdr_exportsCRUDCDR 导出任务
CdrStatscdr_statsGETCDR 统计查询
CdrSummarycdr_summaryGETCDR 汇总查询
CounterscountersGETCPS / 容量计数器
CustomerBlocklistcustomer_blocklistsCRUD客户黑名单
CustomerBlocklistItemscustomer_blocklist_itemsCRUD黑名单条目
Destinationsdestinations⭐ CRUD路由目的地(前缀 + 费率模板绑定)
DialpeersdialpeersCRUD运营商对端(供应商 SIP Trunk 抽象)
DisconnectCodesdisconnect_codesCRUD自定义断线码
Gatewaysgateways⭐ CRUD物理 SIP 网关(供应商/客户中继)
GatewayGroupsgateway_groupsCRUD网关分组
GatewayGroupsDiversionsgateway_groups_diversionsCRUD网关组内 Caller ID 修改规则
NodeRolesnode_rolesCRUDNode 角色(routing/CDR/SBC)
NodesnodesCRUDSEMS 节点(管理面注册)
PoppopCRUDPoP(地理区域)
Rateplansrateplans⭐ CRUD费率模板(连接费 + 计费间隔 + 阶梯价)
RoutingPlansrouting_plans⭐ CRUD路由计划(LCR 选路链)
RoutingPlanStaticRoutesrouting_plan_static_routesCRUD路由计划静态路由(1.15 新增)
RoutingGroupsrouting_groupsCRUD路由分组
RegistrationsregistrationsCRUD外呼 SIP 注册
SchedulersschedulersCRUD定时任务(维护/计费/统计)
ServersserversCRUD服务器信息
TimezonestimezonesGET时区列表
CountriescountriesGET国家列表
CurrenciescurrenciesGET货币列表
LnpDatabaselnp_databaseCRUD号码携带数据库(Number Portability)

3.2 Customer API 端点域

type方法说明
accountsGET / PATCH客户名下账户信息
balancesGET账户余额
cdrsGET客户 CDR 查询
cdr_summaryGET客户 CDR 汇总
rateplansGET客户可用费率
destinationsGET客户目的地费率查询
invoice_templatesGET可用发票模板

3.3 过滤/排序/分页通用约定(Admin + Customer API)

JSONAPI query 参数:

filter[<字段名>]=<值>
filter[<字段名>-eq]=<精确值>
filter[<字段名>-cont]=<包含>
filter[<字段名>-in]=<逗号分隔列表>
filter[<字段名>-gt]/filter[<字段名>-lt]=<比较>
sort=<字段名>          # 升序
sort=-<字段名>         # 降序
page[number]=1&page[size]=25    # 分页
include=<关联type>     # 包含关联(JSONAPI include)
fields[<type>]=a,b,c   # 字段选择

四、认证与首次调用

4.1 Admin API 认证

# Step 1:POST /auth 获取 JWT
curl -s -X POST \
  -H "Content-Type: application/json" \
  -H "Accept: application/vnd.api+json" \
  -d '{"auth":{"username":"admin","password":"111111"}}' \
  https://demo.yeti-switch.org/api/rest/admin/auth

实测响应(节选):

{
  "jwt": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjQ1LCJhdWQiOlsiYWRtaW4iXSwiZXhwIjoxNzc2MzY1NzMzfQ.6aE-JpMXJHOYoqASSk6DbN5Q2bghjkObBcdwsI1HW84"
}

💡 技术拓展:JWT payload 解读 sub=45(Admin User id)、aud=[admin]、exp=...(过期时间 Unix 秒)。默认有效期约 24h,过期后重新请求。

4.2 保存 token 并首次 GET

export YETI_TOKEN='eyJhbGciOiJIUzI1NiJ9...HW84'   # 实际值用上一步 curl 拿到的

# Step 2:用 Authorization: Bearer <jwt> 访问受保护端点
curl -s \
  -H "Accept: application/vnd.api+json" \
  -H "Authorization: Bearer ${YETI_TOKEN}" \
  https://demo.yeti-switch.org/api/rest/admin/accounts?page[size]=3

实测响应(节选):

{
  "data": [
    {
      "type": "accounts",
      "id": "1",
      "attributes": {
        "name": "Demo Contractor",
        "external-id": "CONTRACTOR-001",
        "balance": "10000.0",
        "vat": "0.0",
        "min-balance": "-5000.0",
        "max-balance": "100000.0",
        "contractor-id": 1,
        "invoice-template-id": null
      },
      "relationships": {
        "contractor": { "data": { "type": "contractors", "id": "1" } }
      }
    }
  ],
  "links": { "self": "...accounts?page[number]=1&page[size]=3", "next": "...page[number]=2..." }
  }
}

4.3 Customer API 认证

# Customer API 走 Customer Portal Login(不是 Admin User)
curl -s -X POST \
  -H "Content-Type: application/json" \
  -H "Accept: application/vnd.api+json" \
  -d '{"auth":{"login":"demo-customer","password":"q1w2e3r4"}}' \
  https://demo.yeti-switch.org/api/rest/customer/v1/auth
# 返回同样格式的 {"jwt": "..."}

五、核心 API 域详解

以下每个域给出:endpoint 方法表、curl 实测示例、响应结构、错误码。所有命令基于 demo.yeti-switch.org 实测。

5.1 Accounts(客户账户)—— ⭐ 高频

方法Endpoint说明
GET/accounts列表(支持 filter/page/sort/include)
GET/accounts/{id}详情
POST/accounts新建
PATCH/accounts/{id}更新
DELETE/accounts/{id}删除

新建账户:

curl -s -X POST \
  -H "Accept: application/vnd.api+json" \
  -H "Content-Type: application/vnd.api+json" \
  -H "Authorization: Bearer ${YETI_TOKEN}" \
  -d '{
    "data": {
      "type": "accounts",
      "attributes": {
        "name": "Acme Wholesale",
        "external-id": "ACME-001",
        "balance": "5000.00",
        "vat": "0.00",
        "min-balance": "-2000.00",
        "max-balance": "20000.00"
      },
      "relationships": {
        "contractor": { "data": { "type": "contractors", "id": "1" } }
      }
    }
  }' \
  https://demo.yeti-switch.org/api/rest/admin/accounts

GET + include contractor(避免 N+1):

curl -s \
  -H "Authorization: Bearer ${YETI_TOKEN}" \
  "https://demo.yeti-switch.org/api/rest/admin/accounts?filter[name-cont]=Acme&include=contractor&fields[contractors]=name,billing_contact"

5.2 Balances(余额设置)—— ⭐ 高频

以 external_id 作为主键,只接受 PATCH:

curl -s -X PATCH \
  -H "Content-Type: application/vnd.api+json" \
  -H "Authorization: Bearer ${YETI_TOKEN}" \
  -d '{
    "data": {
      "type": "balances",
      "id": "ACME-001",
      "attributes": { "balance": "8500.00" }
    }
  }' \
  https://demo.yeti-switch.org/api/rest/admin/balances/ACME-001

5.3 BalancesCorrection(余额加减)

curl -s -X PATCH \
  -H "Content-Type: application/vnd.api+json" \
  -H "Authorization: Bearer ${YETI_TOKEN}" \
  -d '{
    "data": {
      "type": "balances_correction",
      "id": "ACME-001",
      "attributes": {
        "correction": "-1500.00",
        "notes": "月度结算扣费"
      }
    }
  }' \
  https://demo.yeti-switch.org/api/rest/admin/balances_correction/ACME-001

5.4 ActiveCalls(活动呼叫)—— ⭐ 运维

方法Endpoint说明
GET/activecalls列表(实时)
GET/activecalls/{id}详情
DELETE/activecalls/{id}强制断呼

强制断呼示例:

# 先查到活动 call id
CALL_ID=$(curl -s -H "Authorization: Bearer ${YETI_TOKEN}" \
  https://demo.yeti-switch.org/api/rest/admin/activecalls?page[size]=1 \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['data'][0]['id'])")

# 断呼
curl -s -X DELETE \
  -H "Authorization: Bearer ${YETI_TOKEN}" \
  https://demo.yeti-switch.org/api/rest/admin/activecalls/${CALL_ID}
# 返回 204 No Content

5.5 Gateways(SIP 网关)—— ⭐ 高频

这是电信平台核心配置实体,字段丰富:

curl -s \
  -H "Authorization: Bearer ${YETI_TOKEN}" \
  "https://demo.yeti-switch.org/api/rest/admin/gateways?filter[pop-id]=1&include=gateway_group" | \
  python3 -m json.tool | head -60

关键字段(attributes):

字段类型说明
namestring网关名称
addressstringSIP 地址(IP 或 FQDN)
portint5060 默认
transport-protocol-idint1=UDP / 2=TCP / 3=TLS / 4=WS
origination-allow / origination-capacitybool/int外呼权限与并发容量
termination-allow / termination-capacitybool/int终呼权限与并发容量
auth-passwordstringSIP Digest 密码
media-encryption-mode-idint1=No / 2=SRTP-SDES / 3=SRTP-DTLS / 4=ZRTP
pidf-lo-mode-idintPIDF-LO 位置信息透传模式(1.15 新增)
pop-idint归属 PoP
radius-auth-profile-idintRADIUS 鉴权 profile
radius-accounting-profile-idintRADIUS 计费 profile

新建网关:

curl -s -X POST \
  -H "Content-Type: application/vnd.api+json" \
  -H "Authorization: Bearer ${YETI_TOKEN}" \
  -d '{
    "data": {
      "type": "gateways",
      "attributes": {
        "name": "SIP-TRUNK-EXAMPLE",
        "address": "sip.example.com",
        "port": 5060,
        "transport-protocol-id": 1,
        "origination-allow": true,
        "origination-capacity": 100,
        "termination-allow": true,
        "termination-capacity": 100,
        "auth-password": "${SIP_TRUNK_PASS}",
        "incoming-auth-usernames": "sip.example.com",
        "pop-id": 1
      },
      "relationships": {
        "gateway_group": { "data": { "type": "gateway_groups", "id": "1" } }
      }
    }
  }' \
  https://demo.yeti-switch.org/api/rest/admin/gateways

5.6 Rateplans + Destinations + RoutingPlans(计费 + 路由)

这三个实体共同构成 LCR 选路链:Rateplan(计费模板)→ Destination(前缀+费率绑定 Rateplan)→ RoutingPlan(串联 Destination + GatewayGroup + 时间规则)。

创建 Rateplan:

curl -s -X POST \
  -H "Content-Type: application/vnd.api+json" \
  -H "Authorization: Bearer ${YETI_TOKEN}" \
  -d '{
    "data": {
      "type": "rateplans",
      "attributes": {
        "name": "Mobile-CN-Rateplan",
        "connect-fee": "0.10",
        "initial-interval": 60,
        "initial-rate": "0.05",
        "next-interval": 30,
        "next-rate": "0.03",
        "markup-profile-id": 1
      }
    }
  }' \
  https://demo.yeti-switch.org/api/rest/admin/rateplans

创建 Destination(前缀 + 绑定 Rateplan):

curl -s -X POST \
  -H "Content-Type: application/vnd.api+json" \
  -H "Authorization: Bearer ${YETI_TOKEN}" \
  -d '{
    "data": {
      "type": "destinations",
      "attributes": {
        "name": "CN Mobile +86",
        "prefix": "86",
        "enabled": true,
        "rateplan-id": 1,
        "routing-group-id": 1,
        "weight": 100,
        "quality-alarm": true,
        "min-accepted-quality": 60
      }
    }
  }' \
  https://demo.yeti-switch.org/api/rest/admin/destinations

创建 RoutingPlan:

curl -s -X POST \
  -H "Content-Type: application/vnd.api+json" \
  -H "Authorization: Bearer ${YETI_TOKEN}" \
  -d '{
    "data": {
      "type": "routing_plans",
      "attributes": {
        "name": "LCR-Main",
        "mode-id": 1,
        "dst_blacklist_id": null,
        "dst_whitelist_id": null
      }
    }
  }' \
  https://demo.yeti-switch.org/api/rest/admin/routing_plans

5.7 CDR 查询(Admin)

# 按时间范围 + 客户 + 目的地查询 CDR
curl -s \
  -H "Authorization: Bearer ${YETI_TOKEN}" \
  "https://demo.yeti-switch.org/api/rest/admin/cdrs?filter[time-start]=2026-10-01T00:00:00Z&filter[time-end]=2026-10-27T23:59:59Z&filter[account-id]=1&sort=-time-start&page[size]=10" | \
  python3 -c "import sys,json; d=json.load(sys.stdin); [print(x['attributes']['time-start'], x['attributes']['src-prefix'],'→',x['attributes']['dst-prefix'], x['attributes']['duration'],'s', x['attributes']['cost']) for x in d['data']]"

5.8 Customer API CDR 查询

curl -s \
  -H "Authorization: Bearer ${CUSTOMER_TOKEN}" \
  "https://demo.yeti-switch.org/api/rest/customer/v1/cdrs?page[size]=100&filter[time-start]=2026-10-01T00:00:00Z"

六、错误码

6.1 HTTP 状态码

状态码含义常见原因
200OKGET 成功
201CreatedPOST 新建成功
202Accepted异步任务(CdrExport)
204No ContentDELETE / PATCH 无返回体
400Bad RequestJSONAPI 格式错误 / 参数缺失
401UnauthorizedJWT 缺失 / 过期 / 无效
403ForbiddenNginx deny all 命中 / Admin User 权限不足
404Not Found资源不存在
409Conflict唯一约束冲突(重名 Gateway)
422Unprocessable Entity业务校验失败(rateplan 关联 gateway 已禁用)
429Too Many Requests限流触发(yeti-web Rack::Attack)
500Internal Server ErrorRails 异常,查看 journalctl -u yeti-web

6.2 JSONAPI 错误体示例

{
  "errors": [{
    "title": "Record not found",
    "detail": "Couldn't find 'Accounts' with 'id'='99999'.",
    "status": "404",
    "code": "not_found"
  }]
}

6.3 生产常见 422 场景

  • 创建 Gateway 时 pop-id 指向不存在的 PoP;
  • 创建 Destination 时 rateplan-id 指向不存在的 Rateplan;
  • 修改 Balance 时 external_id 不存在;
  • RoutingPlan 引用的 RoutingGroup 已被禁用。

七、生产集成建议

7.1 SDK 与客户端

  • yetio(yetisoftware 官方 Python SDK):pip install yetio,封装 JWT 自动刷新;
  • yeti-switch-api PyPI 包:社区维护,v1.2.x 版本对应 Yeti 1.14;
  • HTTP 客户端:Go resty、Python httpx / requests(自动重试 + 指数退避);
  • JWT 缓存:有效期 24h,缓存 23h 主动刷新,避免过期请求 401。

7.2 幂等性

操作幂等 key 建议
POST Gatewaysname 字段做业务唯一约束(yeti-web 已在 DB 层唯一)
PATCH Balancesidempotency-key 自定义 header,yet-web Rack 层检查(需自研)
DELETE ActiveCalls天然幂等(idempotent DELETE)

7.3 限流与重试

# Python requests 示例:指数退避 + 401 自动刷新 JWT
import requests, time

class YetiClient:
    def __init__(self, base_url, username, password):
        self.base_url = base_url
        self.username = username
        self.password = password
        self.token = self._login()
        self.session = requests.Session()
        self.session.headers.update({
            "Accept": "application/vnd.api+json",
            "Content-Type": "application/vnd.api+json",
            "Authorization": f"Bearer {self.token}"
        })

    def _login(self):
        r = requests.post(f"{self.base_url}/auth",
                          json={"auth":{"username":self.username,"password":self.password}})
        return r.json()["jwt"]

    def request(self, method, path, **kw):
        for attempt in range(3):
            r = self.session.request(method, f"{self.base_url}{path}", **kw)
            if r.status_code == 401:
                self.token = self._login()
                self.session.headers["Authorization"] = f"Bearer {self.token}"
                continue
            if r.status_code == 429:
                time.sleep(2 ** attempt)
                continue
            return r
        raise RuntimeError("Max retries exceeded")

client = YetiClient("https://demo.yeti-switch.org/api/rest/admin", "admin", "111111")
r = client.request("GET", "/accounts?page[size]=5")
for acc in r.json()["data"]:
    print(acc["attributes"]["name"], acc["attributes"]["balance"])

7.4 API 日志审计

yet-web Admin UI → Logs → API Log 记录所有 API 请求,默认不存 payload。生产需开启 Debug 以完整审计:

  1. System → API Logs Config → 新建记录;
  2. enabled: true、debug: true;
  3. filter 留空(全部)或按客户端 IP / endpoint 过滤。

⚠️ Debug 日志量大,生产建议定期归档或只对管理网段开启。

7.5 安全红线

红线措施
Admin API Nginx 屏蔽生产 必须 显式 allow 管理网段,deny all
JWT 泄露JWT 存储在 yet-web(Admin User session),Redis 跨机部署需加密传输(Redis auth + TLS)
无频率限制yet-web Rack::Attack 可按 IP / endpoint 配置限流
生产 Admin 密码改默认 admin / 111111,至少 16 位,180 天轮换

7.6 典型集成场景

场景API 端点
CRM 自动开通客户POST /accounts + POST /apiaccesses(自动生成 Customer Portal 凭据)
供应商费率导入批量 POST /destinations + /rateplans(CSV → API 批量导入)
实时监控活动呼叫GET /activecalls?include=gateway&sort=-time-start 每 5s 轮询
自动结算扣费定时 PATCH /balances_correction/{external_id}
CDR 导出到反欺诈系统POST /cdr_exports(指定时间范围 + S3/HTTP 推送)
Number Portability 数据库更新PATCH /lnp_database/{prefix} 批量
客户门户实时余额展示Customer API GET /balances(JWT 为 Customer Portal 登录态)

八、附录:Admin User vs Customer Portal Login

维度Admin UserCustomer Portal Login
创建位置System → Admin UsersSystem → Customer Portal Logins
关联实体无Customer → Account
凭据username + password + 可选 2FAlogin + password + 允许 IP 段
JWT aud 声明["admin"]["customer"]
API 端点/api/rest/admin/*/api/rest/customer/v1/*
权限全局管理员仅关联 Customer 名下 Account

九、参考链接