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-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,Customerhttps://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-apiPyPI 包 - 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 的端点) |
| Accounts | accounts | ⭐ CRUD | 客户账户(计费实体) |
| ActiveCalls | activecalls | ⭐ GET / DELETE | 实时活动呼叫(可远程断呼) |
| ApiAccesses | apiaccesses | CRUD | Customer Portal 登录凭据 |
| Balances | balances | ⭐ PATCH | 账户余额设置(以 external_id 为主键) |
| BalancesCorrection | balances_correction | PATCH | 余额加减 correction |
| CdrExports | cdr_exports | CRUD | CDR 导出任务 |
| CdrStats | cdr_stats | GET | CDR 统计查询 |
| CdrSummary | cdr_summary | GET | CDR 汇总查询 |
| Counters | counters | GET | CPS / 容量计数器 |
| CustomerBlocklist | customer_blocklists | CRUD | 客户黑名单 |
| CustomerBlocklistItems | customer_blocklist_items | CRUD | 黑名单条目 |
| Destinations | destinations | ⭐ CRUD | 路由目的地(前缀 + 费率模板绑定) |
| Dialpeers | dialpeers | CRUD | 运营商对端(供应商 SIP Trunk 抽象) |
| DisconnectCodes | disconnect_codes | CRUD | 自定义断线码 |
| Gateways | gateways | ⭐ CRUD | 物理 SIP 网关(供应商/客户中继) |
| GatewayGroups | gateway_groups | CRUD | 网关分组 |
| GatewayGroupsDiversions | gateway_groups_diversions | CRUD | 网关组内 Caller ID 修改规则 |
| NodeRoles | node_roles | CRUD | Node 角色(routing/CDR/SBC) |
| Nodes | nodes | CRUD | SEMS 节点(管理面注册) |
| Pop | pop | CRUD | PoP(地理区域) |
| Rateplans | rateplans | ⭐ CRUD | 费率模板(连接费 + 计费间隔 + 阶梯价) |
| RoutingPlans | routing_plans | ⭐ CRUD | 路由计划(LCR 选路链) |
| RoutingPlanStaticRoutes | routing_plan_static_routes | CRUD | 路由计划静态路由(1.15 新增) |
| RoutingGroups | routing_groups | CRUD | 路由分组 |
| Registrations | registrations | CRUD | 外呼 SIP 注册 |
| Schedulers | schedulers | CRUD | 定时任务(维护/计费/统计) |
| Servers | servers | CRUD | 服务器信息 |
| Timezones | timezones | GET | 时区列表 |
| Countries | countries | GET | 国家列表 |
| Currencies | currencies | GET | 货币列表 |
| LnpDatabase | lnp_database | CRUD | 号码携带数据库(Number Portability) |
3.2 Customer API 端点域
| type | 方法 | 说明 |
|---|---|---|
accounts | GET / PATCH | 客户名下账户信息 |
balances | GET | 账户余额 |
cdrs | GET | 客户 CDR 查询 |
cdr_summary | GET | 客户 CDR 汇总 |
rateplans | GET | 客户可用费率 |
destinations | GET | 客户目的地费率查询 |
invoice_templates | GET | 可用发票模板 |
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):
| 字段 | 类型 | 说明 |
|---|---|---|
name | string | 网关名称 |
address | string | SIP 地址(IP 或 FQDN) |
port | int | 5060 默认 |
transport-protocol-id | int | 1=UDP / 2=TCP / 3=TLS / 4=WS |
origination-allow / origination-capacity | bool/int | 外呼权限与并发容量 |
termination-allow / termination-capacity | bool/int | 终呼权限与并发容量 |
auth-password | string | SIP Digest 密码 |
media-encryption-mode-id | int | 1=No / 2=SRTP-SDES / 3=SRTP-DTLS / 4=ZRTP |
pidf-lo-mode-id | int | PIDF-LO 位置信息透传模式(1.15 新增) |
pop-id | int | 归属 PoP |
radius-auth-profile-id | int | RADIUS 鉴权 profile |
radius-accounting-profile-id | int | RADIUS 计费 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 状态码
| 状态码 | 含义 | 常见原因 |
|---|---|---|
| 200 | OK | GET 成功 |
| 201 | Created | POST 新建成功 |
| 202 | Accepted | 异步任务(CdrExport) |
| 204 | No Content | DELETE / PATCH 无返回体 |
| 400 | Bad Request | JSONAPI 格式错误 / 参数缺失 |
| 401 | Unauthorized | JWT 缺失 / 过期 / 无效 |
| 403 | Forbidden | Nginx deny all 命中 / Admin User 权限不足 |
| 404 | Not Found | 资源不存在 |
| 409 | Conflict | 唯一约束冲突(重名 Gateway) |
| 422 | Unprocessable Entity | 业务校验失败(rateplan 关联 gateway 已禁用) |
| 429 | Too Many Requests | 限流触发(yeti-web Rack::Attack) |
| 500 | Internal Server Error | Rails 异常,查看 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-apiPyPI 包:社区维护,v1.2.x 版本对应 Yeti 1.14;- HTTP 客户端:Go
resty、Pythonhttpx/requests(自动重试 + 指数退避); - JWT 缓存:有效期 24h,缓存 23h 主动刷新,避免过期请求 401。
7.2 幂等性
| 操作 | 幂等 key 建议 |
|---|---|
| POST Gateways | name 字段做业务唯一约束(yeti-web 已在 DB 层唯一) |
| PATCH Balances | idempotency-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 以完整审计:
- System → API Logs Config → 新建记录;
enabled: true、debug: true;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 User | Customer Portal Login |
|---|---|---|
| 创建位置 | System → Admin Users | System → Customer Portal Logins |
| 关联实体 | 无 | Customer → Account |
| 凭据 | username + password + 可选 2FA | login + password + 允许 IP 段 |
JWT aud 声明 | ["admin"] | ["customer"] |
| API 端点 | /api/rest/admin/* | /api/rest/customer/v1/* |
| 权限 | 全局管理员 | 仅关联 Customer 名下 Account |
九、参考链接
- REST API 文档入口 https://yeti-switch.org/docs/api/
- Demo Admin API 自动生成文档 https://demo.yeti-switch.org/doc/api/admin/index.html
- Demo Customer API 自动生成文档 https://demo.yeti-switch.org/doc/api/customer/v1/index.html
- Demo 系统 https://demo.yeti-switch.org
- JSONAPI 规范 https://jsonapi.org/
- Customer Portal Login 配置 https://yeti-switch.org/docs/web-interface/system/api_accesses.html
- API Log https://yeti-switch.org/docs/web-interface/logs/api_log.html
- Admin Users https://yeti-switch.org/docs/web-interface/system/admin_users.html
- PyPI
yeti-switch-apihttps://pypi.org/project/yeti-switch-api/ - Changelog https://yeti-switch.org/docs/changelog.html