API 和网页工作台使用同一套 Agent、通话记录和 Credits。在工作台里搭好的一切,都可以从你自己的系统里调用,反过来也一样。
| 功能 | 接口 |
|---|---|
| 列出 Agent | GET /api/v1/voice/agents |
| 创建 Agent | POST /api/v1/voice/agents |
| 查看 / 修改 Agent | GET · PATCH /api/v1/voice/agents/{id} |
| 发起一通外呼 | POST /api/v1/voice/calls |
| 列出通话 | GET /api/v1/voice/calls |
| 查看通话(含逐字稿) | GET /api/v1/voice/calls/{id} |
| 删除通话 | DELETE /api/v1/voice/calls/{id} |
| 删除某位来电者的通话 | POST /api/v1/voice/calls/delete-by-caller |
| 外呼任务 | GET · POST /api/v1/voice/campaigns |
| 查看外呼任务 | GET /api/v1/voice/campaigns/{id} |
| 开始 / 暂停 / 取消 | POST /api/v1/voice/campaigns/{id}/{action} |
| 克隆声音 | GET · POST /api/v1/voice/voices |
| 删除克隆声音 | DELETE /api/v1/voice/voices/{id} |
| 拒呼名单 | GET · POST /api/v1/voice/do-not-call |
| 移出拒呼名单 | DELETE /api/v1/voice/do-not-call/{phone} |
| 语言、声音、模板 | GET /api/v1/voice/catalog |
| Agent 的测试 | GET · POST /api/v1/voice/agents/{id}/tests |
| 运行测试 / 查看结果 | POST /api/v1/voice/agents/{id}/test-runs · GET /api/v1/voice/test-runs/{id} |
| 号码接到 Agent | PATCH /api/v1/voice/phone-numbers/{id} |
| 词库 | GET · POST /api/v1/voice/vocabularies |
在工作台“集成 → API 密钥”里创建密钥(也可以在概览页一键创建并复制),以 Bearer 方式发送:
Authorization: Bearer sk-your-key
密钥代表它的创建者,在其所在的工作区里按其角色生效:只读成员的密钥只能读取,不能修改。
从模板创建 Agent,可以在同一个请求里设置字段。不带 template 时从空白开始;GET /api/v1/voice/catalog 列出全部模板,以及 Agent 可用的语言和声音。模板只有英文和中文,其他语言请用该语言写好 system_prompt 和 first_message 一起传。整个请求要么全部保存,要么不创建 Agent,所以被拒绝的请求改正后可以直接重发。返回里的 dashboard_url 是这个 Agent 在工作台的页面,可以在浏览器里直接和草稿通话。所有 Agent 都使用 EvaSay ASR、EvaSay LLM 和 EvaSay TTS,由 EvaSay 按每通电话的语言和通话量自动选择,费用已包含在通话费里,不需要也不能填写其他服务商的账号。
curl https://evasay.com/api/v1/voice/agents \
-X POST \
-H "Authorization: Bearer sk-your-key" \
-H "Content-Type: application/json" \
-d '{
"name": "前台",
"language": "zh",
"first_message": "您好,这里是晨光造型,请问有什么可以帮您?",
"system_prompt": "你是晨光造型理发店的前台……"
}'
用 PATCH /api/v1/voice/agents/{id} 修改这些字段:
| 字段 | 说明 |
|---|---|
name、first_message、system_prompt | 文本 |
status | published 把草稿发布成新版本;draft 停止接听电话 |
language | 27 种语言之一,如 en、zh、es、de;multi 表示跟随来电者 |
temperature | 0-100 |
max_duration_sec、silence_timeout_sec | 通话时长和静音限制 |
interruptible、end_call_enabled | 布尔值 |
confirm_details | 布尔值,复述并确认号码、姓名等关键信息 |
ai_disclosure | 布尔值,默认 true;Agent 在开场白之前用通话语言说明来电者在和 AI 通话(开场白里已经说明的不重复) |
voicemail_detection | 布尔值,外呼时先听对方开口,接到语音信箱就结束 |
voicemail_message | 在语音信箱里留的话,可用 {{变量}};不填则直接挂断 |
transfer_number | 转接号码,国际格式 |
transfer_destinations | 最多再加 9 个号码,[{name, number, when}];Agent 按 when 选择 |
transfer_briefing | 布尔值,接听人先听到 Agent 对这通电话的简要介绍再接通 |
pronunciations | 发音词典,最多 100 条 [{word, say, case_sensitive}] |
idle_messages | 最多 3 条静音提醒语,依次说完仍无回应则挂断 |
noise_reduction | 布尔值,先滤掉来电者那边的背景噪音再识别 |
recording_enabled | 布尔值,双声道录音(左声道来电者,右声道 Agent) |
recording_consent | announce、ask(来电者同意才保存)或 none |
recording_notice | 开头说的告知语或问题;不填则按通话语言使用内置说法 |
handoffs | 最多 5 个 [{agent_id, when}]:可以把通话交给的其他 Agent |
knowledge_base_id | 你的某个知识库 |
vocabulary_id | 你的某个词库,来电者说到其中的词时识别更准 |
voice_id | 声音:EvaSay 内置声音的 ID,或你的克隆声音(clone:…) |
内置声音按语言区分:中文有 ming-designed-female-clear、ming-designed-female-warm(女声)和 ming-designed-male-clear、ming-designed-male-friendly(男声),其他语言有 F1–F5(女声)、M1–M5(男声)。设成不说该语言的内置声音时,会换成同性别、说该语言的声音。其他值,以及还没准备好的克隆声音,会被拒绝。旧字段 tts_voice_id 仍然可用。
analysis_config 设置每通电话结束后要分析什么:summary(默认 true)、summaryLanguage、successCriteria(怎样算成功)、fields(最多 20 个要提取的字段 {name, type, description},type 为 string、number 或 boolean)和 scorecard(最多 10 条 {name, description, weight},权重 1–5)。结果在通话的 analysis 和 call.analyzed Webhook 里。
修改 Agent 的字段只改草稿,通话继续使用已发布的版本,直到你用 POST /api/v1/voice/agents/{id}/publish(可带 note)发布草稿,或在同一次 PATCH 里带上 status: "published"。每次发布都会保存一个带编号的版本,Agent 的 published_version 表示通话使用哪个版本。一通电话始终使用它开始时线上的版本,通话中发布不会影响它。工作台里的测试通话使用草稿。
GET /agents/{id}/versions 列出所有版本,POST /agents/{id}/versions/{version}/restore 回滚:新的通话改用该版本,草稿也恢复成它。Agent 的名称、部署位置、挂件外观和通知设置不分版本,修改后立即生效;知识库里的文档同样如此。
一个测试用文字模拟一通电话:模拟来电者按 scenario 扮演,Agent 用草稿(指令、开场白和知识库)回答,再由评审按 success_criteria 判断这通是否合格。给重要的通话场景写测试,每次修改后运行,全部通过再发布。
curl https://evasay.com/api/v1/voice/agents/AGENT_ID/tests \
-X POST \
-H "Authorization: Bearer sk-your-key" \
-H "Content-Type: application/json" \
-d '{
"name": "改约",
"scenario": "老顾客想把周五下午 3 点的预约改到周六上午。",
"success_criteria": "给出周六可约的时间,并确认新的时间和顾客姓名。",
"max_turns": 8
}'
POST /agents/{id}/test-runs 运行 Agent 的全部测试(或 test_ids 里的几个),每个测试返回一次运行(status: "running")。轮询 GET /test-runs/{id},直到 status 变成 passed、failed 或 error,这时运行里有 transcript 和评审的 reason(默认英文,reason_language: "zh" 为中文)。每次运行消耗点数,数额在工作台的测试页显示;余额不够时从付不起的那个开始不再运行,not_started 列出没运行的测试。没跑完的运行会退回点数。测试检查的是 Agent 说了什么,不检查声音和时延,也不会真的调用函数:函数请用 POST /tools/{id}/test 预览。每个 Agent 最多 30 个测试,PATCH、DELETE /agents/{id}/tests/{test_id} 修改或删除。
除了完整通话(type: "simulation",默认),测试也可以只检查一句回复。type: "next_reply" 需要 chat_history,即这句回复之前、以来电者结尾的对话([{role, text}],role 为 caller 或 agent),以及判断 Agent 下一句回复的 success_criteria。type: "tool_call" 需要 chat_history 和 expected_tool {name, arguments, match}:Agent 应该调用 name(它的某个函数,或 end_call、transfer_call、handoff_to_agent),并传入 arguments;参数按精确值比较(去掉首尾空格、不区分大小写),设为 match: "judge" 时按意思比较;Agent 多传的参数不检查。运行的 transcript 里,tool_calls 记录 Agent 调用了哪些函数。
POST /agents/{id}/tests/generate 根据 Agent 的草稿(指令、知识库和函数)生成测试草稿。参数有 count(1–10,默认 5)、types、focus(要覆盖的情况)和 language(en 或 zh,用于名称和标准),按一次运行扣点数,返回的草稿可以检查后提交给 POST /agents/{id}/tests;带 save: true 时直接保存。
hooks 在通话的特定时刻自动做一件事,最多 10 条:on: "silence"(Agent 说完后来电者沉默 seconds 秒,3–600;来电者再开口前只触发一次)、on: "time_elapsed"(通话进行到第 seconds 秒,10–3600)或 on: "call_ending"(通话结束时)。每条的 do 最多 3 个动作:{type: "say", text}、{type: "transfer", destination?}、{type: "end_call", text?}、{type: "tool", name, args?};通话结束时的规则只能调用函数。被规则结束的通话 ended_reason 为 rule_ended。
电话通话里,来电者按的键会作为回答交给 Agent:按 # 或停顿两秒算一次输入结束。外呼时,Agent 也能按键走对方公司的电话菜单。
transfer_number 是转接人工的主号码,余额用完时来电也转到这里。transfer_destinations 可以再加几个带名称的号码,每个写上什么时候转(例如“财务:发票和退款问题”),由 Agent 选择。打开 transfer_briefing 后,Agent 转接时写一段简短介绍,接听人接起后先听到这段介绍(EvaSay 的声音,语言与通话摘要相同),再接通来电者。这类通话的 ended_reason 为 transferred。
设置 handoffs 后,Agent 可以把通话交给工作区里另一个已发布的 Agent,例如前台把预约交给预约 Agent,按 when 选择。接手的 Agent 知道之前说过的所有内容,用自己的提示词、声音、知识库、工具和交接设置继续这通电话。通话仍记在第一个 Agent 名下(列表、分析和计费),逐字稿里会标出每次交接。
号码在工作台里用你自己的运营商账号连接。GET /api/v1/voice/phone-numbers 列出号码,PATCH /api/v1/voice/phone-numbers/{id} 传 { "agent_id": "AGENT_ID" } 把这个号码的来电交给某个 Agent(使用它已发布的版本),传 "agent_id": null 则不接任何 Agent;label 修改号码名称。
词库里放来电者会说、但识别容易听错的名称和术语,例如产品名、药名、街道名。POST /api/v1/voice/vocabularies 传 { "name": "产品", "terms": ["Lumora", "Vantix Pro"] }(最多 1,000 个词,每个不超过 50 个字符,也可以用 "preset": "medical" 或 "legal" 起步),再把 Agent 的 vocabulary_id 设成它。GET、PATCH(name、terms)和 DELETE /vocabularies/{id};还有 Agent 在用的词库不能删除。
在“电话号码”页可以给每个号码设置来电规则,在 Agent 接听之前生效:拦截的号码或号码开头(如 +1555)以及隐藏号码的来电不接听、不计费;指定的来电者可以直接转到另一个号码;Agent 无法接听时(未发布、没有 Worker、点数用完且没设转接号码)可以转接到一个号码或播放你自己的一段话。通话开始时查询(run_at_start)也可以拒接:返回 {"reject": true, "message": "..."} 时 Agent 说完这句话(如有)就挂断,ended_reason 为 rejected。
从工作区的某个电话号码打出一通电话。每个请求需要一个唯一的 Idempotency-Key;用同一个值重发会返回同一通电话,不会重复拨打。
curl https://evasay.com/api/v1/voice/calls \
-X POST \
-H "Authorization: Bearer sk-your-key" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "AGENT_ID",
"to": "+14155550100",
"variables": { "customer_name": "王先生", "order_id": "1234" }
}'
工作区有多个电话号码时必须填 from。variables 用来替换开场白和提示词里的 {{customer_name}} 这类占位符。
GET /api/v1/voice/calls/{id} 返回通话状态、时长、扣除的 Credits、摘要、分析结果和逐字稿。用 GET /api/v1/voice/calls?agent_id=&status=&channel=&user_id=&page= 列出通话(user_id:你给这个人的 ID,来自网页通话令牌)。返回里的 turns 是逐轮调试日志:每一轮从来电者说完到听到回答的各段耗时(毫秒),包括 waitMs(等来电者说完)、sttMs(识别)、llmMs(模型首字,每轮调用函数会多一个)、ttsMs(语音首包)、firstAudioMs(听到回答),以及调用的函数、是否被打断和出错信息。工作台通话详情页也会显示。
打开 recording_enabled 后,每通电话保存为双声道 WAV:左声道是来电者,右声道是 Agent,采样率与通话一致。recording_consent 为 "announce" 时 Agent 先说明通话会被录音;为 "ask" 时先询问,来电者同意才保存录音(不回答视为不同意);为 "none" 时不提示。请先确认你和来电者所在地区的法律要求。
录音默认存放在 EvaSay,也可以在“集成 → Webhook 与通知 → 录音存储”里改存到你自己的 S3 兼容存储桶(AWS S3、Cloudflare R2、Google Cloud Storage、Supabase、MinIO)。录音由运行通话的 Worker 直接上传到存储桶。录音保存后,GET /api/v1/voice/calls/{id} 返回 1 小时内有效的 recordingUrl,以及 recording.status(recording、stored、declined 或 failed)。
DELETE /api/v1/voice/calls/{id} 删除一通已结束的通话,连同逐字稿、录音(EvaSay 存储或你自己的存储桶里的)、分析和 Webhook 投递记录。已用的 Credits 仍保留在点数记录里。暂时不能删除的通话返回 409,code 为 call_not_deletable_yet:通话还在进行或刚刚结束、还在结算、录音可能还在上传,或者欠着余额不够支付的 Credits(先充值)。
有人要求删除自己的数据时,用 POST /api/v1/voice/calls/delete-by-caller:传 { "phone_number": "+14155550123" } 删除这个号码的所有通话(呼入看来电号码,外呼看被叫号码),传 { "user_id": "..." } 删除令牌里带这个 user_id 的所有网页通话。返回 { "deleted": 12, "kept_back": { "settling": 1 }, "more": false }:每次最多 200 通,more 为 true 时再调一次;kept_back 里的通话稍后再删。号码如果在拒呼名单里,会继续留在名单上。工作台的“通话记录 → 删除来电者的数据”做的是同一件事。
通话进行中时调用 POST /api/v1/voice/calls/{id}/control。运行这通电话的 Worker 约 2 秒内取到指令,接口返回 202。联系不上运行这通电话的 Worker 时,接口返回 503(code call_unreachable),指令不会执行。
type | 作用 |
|---|---|
say | Agent 说完当前这句话后说出 text;带 "interrupt": true 时立即打断并说出 |
note | 把 text 告诉 Agent(不读出来),从下一句回复起参考它;逐字稿里会标出这条提示 |
end | Agent 先说出 text(如果有)再挂断;电话通话不带 text 时立即挂断 |
这类通话的 ended_reason 为 api_ended。工作台的通话详情页也有同样的控制按钮。
POST /api/v1/voice/calls/{id}/listen(可带 { "channels": 2 })返回 listen_url,2 分钟内可以连接。这是一个 WebSocket:先收到一条 JSON {"type":"start","sampleRate":8000,"channels":1,"encoding":"pcm_s16le"},之后是二进制的 16 位小端 PCM,即来电者听到的内容(双方混音;channels: 2 时左声道是来电者、右声道是 Agent)。通话结束时连接关闭。音频直接来自运行通话的 Worker,不经过 EvaSay 的控制面;工作台通话详情页的“旁听”按钮用的是同一个地址。
用你自己的程序直接收发通话音频,例如接入自建的电话系统或 App。先创建通话:
curl https://evasay.com/api/v1/voice/calls/websocket \
-X POST \
-H "Authorization: Bearer sk-your-key" \
-H "Content-Type: application/json" \
-d '{ "agent_id": "AGENT_ID", "encoding": "pcm_s16le", "sample_rate": 16000 }'
返回的 websocket_url 带一次性令牌,15 分钟内连接有效。encoding 为 pcm_s16le(16 位小端 PCM,sample_rate 可选 8000、16000、24000)或 mulaw(8 kHz μ-law)。两个方向都用这个格式的二进制帧传音频,建议每帧 20 毫秒。文本帧是 JSON:
| 方向 | 消息 |
|---|---|
| 你 → EvaSay | {"type":"dtmf","digits":"12#"} 按键;{"type":"end"} 挂断;{"type":"text","text":"…"} 客户打的字(点回话按钮时加 "input":"button");{"type":"context","text":"…","key":"cart"} 你的 App 告诉 Agent 的情况(Agent 不会因此开口);{"type":"tool_result","id":"…","result":{…}}(或 "error")页面函数的结果 |
| EvaSay → 你 | start(含 callId);clear(来电者打断了,丢掉还没播放的 Agent 音频);transcript(role、text、final,打字的带 input);user_started_speaking、user_stopped_speaking、agent_started_speaking、agent_stopped_speaking;cards(cards、quick_replies);tool(id、name、arguments:执行你的一个页面函数);notice(code:打字太快 too_fast 或到上限 limit);ended(含 reason,随后关闭连接) |
Agent 收到连接就开始说开场白。请求里还可以带 user_id 和 page_tools(你的 App 会执行的页面函数,见下文)。按通话分钟计费,与其他通话相同,通话记录、分析和 Webhook 也一样。
网页通话(挂件、托管通话页、你的 App)里,客户在语音之外还看得到一个对话框,所以 Agent 能做的不只是说话。电话通话没有这些功能。
打开 Agent → 工具 → 发给客户(Agent API 里是 send_to_caller),Agent 一次可以给客户发最多 5 张卡片:标题、标题下的一行、说明、图片、链接,每张最多 2 个回话按钮;问问题时还可以附上最多 3 个快捷回复。链接不会被读出来。
Agent 只能发这通电话里拿到过的链接和图片:它的指令和开场白、知识库(从网页读入的文档带着网址)、你的函数返回的结果、预设卡片。链接还必须在 allowed_hosts 里的网站上(预设卡片自己的网站总是可以)。Agent 编的、或者客户念出来的链接都不会发出去。Agent 说的话里出现这些网站的链接,对话框里可以直接点。
"send_to_caller": {
"enabled": true,
"allowed_hosts": ["shop.example.com", "*.cdn.example.com"],
"presets": [
{ "name": "hours", "when": "客户问营业时间时", "title": "营业时间",
"description": "周一至周六 9:00–18:00", "url": "https://shop.example.com/stores", "image_url": "" }
]
}
每次发送都会触发 call.message_sent Webhook(channel、带 title 和 url 的 items、quickReplies、blocked)。一通电话最多发 10 次。
通话中客户也可以打字(邮箱、订单号)或点回话按钮,Agent 像听到客户说话一样回答,正在说话时先停下。每条最多 500 字,每秒 1 条,每通电话 100 条。通话记录里会标出这一轮是打字还是点按钮。
挂件在你的网页上提供一个 evasay 对象。全部可选;只有网页在挂件的允许嵌入网站里(Agent → 部署),通话窗口才会接收它的消息:
<script src="https://evasay.com/voice-widget.js" data-agent="AGENT_SLUG" async></script>
<script>
window.evasay = window.evasay || [];
evasay.push(function (agent) {
agent.setVariables({ plan: 'pro', page: location.pathname }); // 给下一通电话
agent.update('购物车:2 件,合计 $120', { key: 'cart' }); // 通话中
agent.registerTool('add_to_cart', async function (args) {
// 你的代码,在访客的浏览器里执行
return { ok: true, checkout: 'https://shop.example.com/checkout' };
});
agent.on('cards', function (cards) {}); // 还有 'live'、'ended'
});
</script>
setVariables 的值用来替换指令和开场白里的 {{name}}。它们来自访客的浏览器,所以会告诉 Agent 这些值未经核实,也不会传给你的函数和来电前查询。update 告诉 Agent 页面上发生了什么,Agent 不会因此开口;带同一个 key 的新内容替换旧的。"kind": "page",没有 url),再在网页上注册同名函数。只有注册了它的网页发起的通话里 Agent 才能调用;默认最多等 timeout_sec(1–10,默认 5)秒拿结果,wait_for_result 为 false 时不等。它以访客自己的登录状态执行,相当于访客自己点了一下:花钱或不可撤销的操作请在你的页面里先让访客确认。返回结果里的链接可以发成卡片。把 谁可以发起通话 设为 只允许带令牌,由你的服务器为登录用户申请一次性令牌:
curl https://evasay.com/api/v1/voice/web-calls \
-X POST \
-H "Authorization: Bearer sk-your-key" \
-H "Content-Type: application/json" \
-d '{ "agent_id": "AGENT_ID", "user_id": "u_42", "variables": { "name": "Ann" }, "expires_in": 600 }'
返回 token(只能用于一通电话,expires_in 秒内有效,30–3600,默认 600)、agent(slug)和 web_call_url。把令牌交给网页:在挂件的 script 上加 data-token-url="/your/endpoint"(挂件带着访客的 cookie 向这个地址发 POST,读取 { "token": "…" }),或者 agent.setToken(async () => token)。令牌里的 variables 是可信的:指令、来电前查询和函数参数模板都可以用,user_id 也一样,通话会记下它(通话里的 userId,列表用 user_id 筛选)。API 密钥只放在你的服务器上。
在你自己的 App 里,把 { "token": "…", "pageTools": ["add_to_cart"] } POST 到 https://evasay.com + web_call_url,得到这通电话的连接信息(WebRTC offer 地址或 WebSocket 地址,带通话令牌),用 RTVI 客户端连接,例如 Pipecat 的 Web、iOS、Android、React Native 客户端 SDK。卡片、页面函数和提示以 RTVI server-message 发来(evasay.cards、evasay.tool、evasay.notice);打字用 send-text 发送,页面情况和函数结果用 client-message 的 evasay.context、evasay.tool_result 发送。也可以用音频 WebSocket自己传音频,带上 user_id 和 page_tools。
在“电话号码 → SIP”里可以让自己的 PBX、SIP 电话或 SIP 中继直接把电话打给 Agent,话费由你的 SIP 服务商收取:
sip:ev…@<EvaSay 的 SIP 域名>,只接呼入,适合 PBX(3CX、FreePBX、Asterisk 等)和 SIP 电话。from 写这个号码);转人工通过 REFER 交给中继完成。中继要求认证时,EvaSay 用你填写的中继用户名和密码回应。打进来的电话默认要用 EvaSay 给的用户名和密码做摘要认证(realm 是 SIP 域名),也可以另外限定来源 IP,或在服务商不支持认证时只按 IP 放行(此时必须填写 IP)。每个 SIP 地址或中继都有同时通话上限,超出时返回 486。密码只在创建或重设时显示一次。目前只支持 UDP 和 G.711(PCMU、PCMA),按键支持 RFC 4733 和 SIP INFO;不要在 PBX 的中继上开启注册(REGISTER)。
外呼任务用一个 Agent 按名单拨打,遵守呼叫时段和并发上限(max_concurrency,每个任务最多 100 路同时呼叫,且不超过工作区的同时通话上限),没接通的按规则重试。
curl https://evasay.com/api/v1/voice/campaigns \
-X POST \
-H "Authorization: Bearer sk-your-key" \
-H "Content-Type: application/json" \
-d '{
"name": "预约提醒",
"agent_id": "AGENT_ID",
"phone_number_id": "PHONE_NUMBER_ID",
"timezone": "America/New_York",
"window_start": "09:00",
"window_end": "18:00",
"max_concurrency": 2,
"max_attempts": 2,
"retry_delay_min": 60,
"consent_confirmed": true,
"contacts": [
{ "phone": "+14155550100", "name": "Alex", "appointment": "周二下午 3 点" }
]
}'
名单也可以是 CSV 文本,第一列是电话号码。必须带上 consent_confirmed: true:表示你确认名单上的每个人都同意接听来电(包括 AI 语音来电),EvaSay 会把这次确认和任务一起保存(consent_confirmed_at)。任务创建后是草稿,用 POST /api/v1/voice/campaigns/{id}/start 开始,pause、cancel 同理。开始前 Agent 必须已发布。
Agent 开着 voicemail_detection(默认开)时,外呼接通后先听对方开口再说开场白;对方一直不说话,等两三秒后照常开场。听出是语音信箱或运营商的录音提示时,等提示音结束后留下 voicemail_message,没填就直接挂断。这样的通话 ended_reason 为 voicemail,只按实际时长扣点数,不做通话分析,也不发通话通知。
外呼任务里,接到语音信箱的号码和没接通一样按规则重拨,只在最后一次拨打时留言,同一个人不会收到好几条一样的留言。
每一通外呼,不管来自外呼任务还是 POST /api/v1/voice/calls,拨号前都会先查工作区的拒呼名单。名单上的号码不会被拨打:API 返回 409,code 为 do_not_call;外呼任务把这个联系人标为 canceled、outcome 为 do_not_call,接着拨下一个。
Agent 外呼时对方要求不要再打来,Agent 会自己把号码加进名单并告诉对方,同时发送 call.do_not_call Webhook。也可以在 外呼任务 → 拒呼名单 里或用 API 添加、移除号码:
curl https://evasay.com/api/v1/voice/do-not-call \
-X POST \
-H "Authorization: Bearer sk-your-key" \
-H "Content-Type: application/json" \
-d '{ "phones": ["+14155550100"], "note": "邮件里要求不再来电" }'
号码用国际格式,一次最多 1000 个。GET /api/v1/voice/do-not-call 按加入时间从新到旧列出(可带 search、limit、page),每条带来源 source(call、dashboard 或 api)。DELETE /api/v1/voice/do-not-call/%2B14155550100 把号码移出名单;只在对方同意再接电话时才这样做。
POST /api/v1/voice/voices 用一段单人说话的 WAV 录音制作声音(multipart:audio、name、language、consent=true)。返回 202,状态为 running;轮询 GET /api/v1/voice/voices 直到 ready,再把 Agent 的 voice_id 设为它的 voice_id。language 填录音里说的语言,可以是上文 Agent 支持的任意一种语言;做好的声音能说其中除中文外的所有语言(中文由同性别的内置声音来说),在录音的语言里最像本人。每个克隆声音消耗 Credits,数量在工作台里创建前可见。
在“集成 → Webhook 与通知”里添加 https 地址,接收通话事件:
| 事件 | 何时发送 |
|---|---|
call.started | Agent 接起通话 |
call.ended | 通话结束,包含时长和点数 |
call.analyzed | 摘要、结果、提取字段和逐字稿已生成 |
call.transcript | 通话进行中,每隔几秒发送新增的逐字稿 |
call.tool_called | Agent 调用了你的函数:name、arguments、status、durationMs |
call.handed_off | 通话交给了另一个 Agent:fromAgentId、toAgentId |
call.transferred | 通话转接给了真人:destination |
call.voicemail | 外呼接到了语音信箱 |
call.do_not_call | 外呼时对方要求不要再打来:phone |
call.recording_ready | 双声道录音已保存:recordingUrl(24 小时内有效)、durationMs |
call.message_sent | Agent 在网页或 App 通话里给客户发了卡片:items、quickReplies |
alert.triggered | 你设置的告警(Webhook 与通知 → 告警)越过了告警线 |
alert.resolved | 该告警恢复正常 |
通话进行中的事件在 data.call 里带上通话信息(id、agentId、channel、direction、号码)。每个请求都带签名:
EvaSay-Signature: t=<unix 秒>,v1=<对 "<t>.<原始请求体>" 的 HMAC-SHA256 十六进制>
EvaSay-Delivery: <投递 ID>
用创建 Webhook 时显示的密钥校验签名,并返回 2xx 状态码。投递失败会在 1 分钟、5 分钟、30 分钟、2 小时和 12 小时后重试,请求体和 EvaSay-Delivery 不变,可以据此去重。每个 Webhook 最近的投递记录(实际发送的内容和你的接口返回的状态码)可以在工作台查看,也可以用 GET /webhooks/{id}/deliveries 读取;POST /webhooks/{id}/deliveries/{delivery_id} 会把某一条再发一次。
告警除了邮件和 alert.triggered / alert.resolved Webhook,还可以在“Webhook 与通知 → 告警 → 同时发到”里发到 Slack 频道(Incoming Webhook)、企业微信群机器人或 Telegram。
错误统一为:
{
"error": {
"type": "invalid_request_error",
"code": "invalid_agent_field",
"message": "status must be draft or published",
"param": "status"
}
}
| 状态码 | 含义 |
|---|---|
| 400 | 输入无效(code 和 param 说明是哪个字段) |
| 401 | 缺少或无效的 API 密钥 |
| 402 | Credits 不足,无法开始通话 |
| 403 | 密钥只读,或 Agent 不可用 |
| 404 | 工作区里没有这个 Agent、通话或外呼任务 |
| 409 | 状态不对(如开始一个正在运行的任务)、服务商账号未连接,或号码在拒呼名单上(do_not_call) |
| 429 | 请求过多,请放慢后重试 |
工作区最近 30 天的 Voice API 请求(时间、接口、状态码、耗时、使用的密钥和错误码,不含请求和返回内容)可以在“API 密钥”页的“API 请求日志”里查看。
读取每个密钥每分钟最多 120 次,写入和外呼每分钟最多 30 次。
通话按分钟从工作区的 Credits 扣费,和工作台共用同一个余额。当前费率见定价页。
把 OpenAPI 接口描述交给 AI 编程工具,生成集成代码。公开 API、工作台和 MCP 管理入口使用同一工作区资源与权限。
| 功能 | 接口 |
|---|---|
| 可复用工具 | GET / POST /api/v1/voice/tools |
| 查看 / 修改 / 删除工具 | GET / PATCH / DELETE /api/v1/voice/tools/{id} |
| 预览或实测工具 | POST /api/v1/voice/tools/{id}/test |
| 绑定到 Agent | PUT /api/v1/voice/agents/{id}/tools |
| 知识库 | GET / POST /api/v1/voice/knowledge-bases |
| 查看 / 修改 / 删除知识库 | GET / PATCH / DELETE /api/v1/voice/knowledge-bases/{id} |
| 添加 / 列出文档 | POST / GET /api/v1/voice/knowledge-bases/{id}/documents |
| 查看 / 删除文档 | GET / DELETE /api/v1/voice/knowledge-bases/{id}/documents/{doc_id} |
| 通话事件订阅 | GET / POST /api/v1/voice/webhooks |
| 查看 / 修改 / 删除订阅 | GET / PATCH / DELETE /api/v1/voice/webhooks/{id} |
| 发送带签名的测试事件 | POST /api/v1/voice/webhooks/{id}/test |
| 发现外部 MCP 工具 | POST /api/v1/voice/mcp/discover |
| MCP 管理入口 | /api/v1/voice/mcp |
通过 POST /tools 注册自己的 CRM 查询接口,例如:
{
"name": "lookup_customer",
"description": "确认客户编号后查询客户资料。",
"kind": "http",
"url": "https://crm.example.com/customers/{{customer_id}}",
"method": "GET",
"read_only": true,
"credential_ref": "crm",
"input_schema": {
"type": "object",
"properties": { "customer_id": { "type": "string" } },
"required": ["customer_id"],
"additionalProperties": false
},
"response_path": "data"
}
创建结果为 { "data": { "id": "TOOL_ID", ... } }。向 PUT /agents/AGENT_ID/tools 发送 { "tool_ids": ["TOOL_ID"] },将工具定义的快照绑定到 Agent,并替换其全部工具。工具库更新后需重新绑定;删除库中定义会保留已绑定的快照。也可以通过 Agent API 的 tools 字段读取和替换内联工具定义,通过 analysis_config、widget_config 配置分析与挂件。
工具支持 GET、POST、PUT、PATCH、DELETE。query 和 request_body 可使用嵌套 JSON 模板,例如 { "customer": { "id": "{{customer_id}}" } };完整占位符保留原始 JSON 类型。URL 参数会编码,域名必须是固定的公网 HTTPS 地址。未设置 request_body 时,非 GET 请求发送 { "arguments": {...}, "call": { "callId": "...", "agentId": "...", "toolCallId": "...", "idempotencyKey": "..." } }。response_path 用点号选择返回字段。简单 parameters 继续兼容;input_schema 还支持嵌套对象、数组、枚举和数字范围,不支持外部 Schema 引用或正则表达式。
static_args:每次调用都带上的值,模型不能改,比如门店编号;也可以在地址和请求体里用 {{名称}} 引用。extract:[{name, path}] 从函数返回里取出字段(如 data.id),后面的函数可以用 {{name}} 引用,比如先查客户得到 customer_id 再用它下单,不用模型复述。通话开始时查询返回的字段也同样保存。reject_when:最多 5 条规则 {on: caller|agent, pattern, negate},全部成立时不执行函数,并把 reject_message 告诉 Agent。model_says:每次调用时由模型写一句话,在函数运行时说给来电者,代替固定的 speak_before。在“集成 → Webhook 与通知 → 连接的应用”里连接应用,再到 Agent 的“知识库与工具”里点“添加现成工具”,不用自己写接口:
check_availability(按日期查空闲时段)和 book_appointment(来电者选定后直接预约)。Google 日历按你设置的时区、营业日、营业时间和每次时长计算空闲时段;Cal.com 使用活动类型自己的可预约时间,来电者没有提供邮箱时使用你设置的备用邮箱。save_to_sheet,按你列出的表头逐列填写一行,并自动补上通话 ID 和时间。notify_team,把 Agent 写的一条消息发到 Slack 频道(Incoming Webhook)。这些函数由 EvaSay 控制面使用你连接的账号执行,Worker 和 Agent 定义里都不保存应用的凭据。通过 API 读取 Agent 时它们显示为 kind: "app"(带 app、action、connection_id、app_config);替换 tools 时原样带回即可保留。连接应用只能在工作台完成。
给 Agent 的某个工具设置 run_at_start: true,每通电话一开始、Agent 打招呼或回答之前就会先调用它。参数名为 caller_number、called_number、direction、channel 或 call_id 时会自动填入。返回的字段会填进 first_message、system_prompt 和 idle_messages 里的 {{变量}}(下一层的字段写成 {{customer.name}}),整个结果也作为背景交给 Agent。开场白里有变量时最多等 3 秒;查不到的变量留空。每个 Agent 只能有一个这样的工具。
凭据由服务器环境按工作区和目标地址配置,需要时联系 support@evasay.com 开通 credential_ref。不要把密钥写入 Agent 定义、Header、提示词或工具参数。CRM 本身的密钥也可以留在你自己的接口服务环境里。
向 POST /tools/TOOL_ID/test 发送以下内容,先预览请求:
{
"arguments": { "customer_id": "customer-123" },
"dry_run": true,
"mock_result": { "name": "Alex", "order_status": "shipped" }
}
预览会校验参数并显示请求映射,不运行对话,也不请求业务系统。设置 dry_run=false 才会真实执行。真实写操作还需要 confirmed=true 和 idempotency_key(8–128 位字母、数字、点、下划线、冒号或连字符)。结果包括 ok、status、duration_ms 和有大小限制的结果,不返回凭据。读取和写入工具的测试均要求工作区写权限。
通话中的业务请求带稳定的 Idempotency-Key,你的接口应按它去重。同一通话里相同参数的写操作会去重,超时后结果未知的操作也不会自动重放。确实需要执行另一笔相同业务时,应使用不同业务参数,如 operation_id。调用事件记录状态和耗时;现有“对话测试”仍是文本模拟,不能代替工具接口实测。
添加 Streamable HTTP MCP 连接:
地址:https://evasay.com/api/v1/voice/mcp
鉴权:Authorization: Bearer <你的 EvaSay API 密钥>
例如在 Claude Code 里:
claude mcp add --transport http evasay https://evasay.com/api/v1/voice/mcp --header "Authorization: Bearer $EVASAY_API_KEY"
连接后会告诉 AI 工具怎样搭 Agent:先读取可选的语言、声音和模板,建草稿,加知识库、函数和词库,写测试并运行,改草稿直到通过,把 Agent 的 dashboard_url 给你试打,等你确认后才发布和接号码。会读文档的工具可以从 https://evasay.com/llms.txt 开始,里面有本页、OpenAPI 描述和 MCP 地址。
使用客户端的安全密钥或环境变量配置,不把密钥粘贴到提示词。入口使用 API 密钥,不提供 OAuth 登录。可读取可选值,管理 Agent、测试、工具、知识库、词库、号码分配、Webhook 和通话记录。发布 Agent 和真实外呼都是写操作,外呼会消耗 Credits 并产生运营商费用;每次调用都会重新检查工作区权限。请开启客户端的写操作确认。该入口返回无状态 JSON 响应,不提供后台通知流。
可以对 AI 编程工具说:“帮我搭一个 EvaSay Agent,用英语和西班牙语接美发店的电话,通过我的预约接口约剪发。给预约、改约、取消各写一个测试,跑到全部通过,再把 Agent 页面给我试打。”检查配置后再明确授权发布和真实通话。
向 POST /mcp/discover 发送 { "url": "https://crm.example.com/mcp", "credential_ref": "crm" } 获取工具列表和参数 Schema,然后只注册所需工具:
{
"name": "lookup_customer",
"description": "按已确认的客户编号查询 CRM。",
"kind": "mcp",
"url": "https://crm.example.com/mcp",
"mcp_tool": "get_customer",
"credential_ref": "crm",
"read_only": true,
"input_schema": {
"type": "object",
"properties": { "customer_id": { "type": "string" } },
"required": ["customer_id"]
}
}
只有注册的工具会开放给 Agent。外部连接支持 Streamable HTTP、请求的 JSON 或 SSE 响应、公网 HTTPS 地址和预先配置的凭据。暂不支持旧式 SSE 入口、OAuth 协商、服务端发起的 sampling/elicitation 或二进制附件。发现工具不会自动开放全部权限;远端工具的 Schema 需要是受支持的内联 JSON Schema。
向 POST /webhooks 发送 { "url": "https://crm.example.com/evasay-events", "events": ["call.analyzed"] }。结果包含订阅和只在创建时返回的签名密钥。接收端验证 EvaSay-Signature,按 EvaSay-Delivery 去重,再更新 CRM。通话事件 Webhook 用于异步通知;通话中的工具请求直接把执行结果返回给 Agent。