# HEYIN 开放 API 技术接入文档

文档版本 2026-11-07 · 内容校验 cc2b2d389566ae21c0c527333e3350b9722645eb330596c6a8eae2fa7a7c018f

[接口规范JSON](/openapi.json) · [开发者控制台](/app#developer)
## 快速开始
通过服务端HTTP请求使用公共音色或注册Key私有音色。生产地址统一为 `https://www.heyintts.cn/api/open`。

1. 打开[开发者控制台](/app#developer?tab=keys)，创建API Key，明文只展示一次。
2. 使用 `/account` 查询余额/额度、`/clone/models` 查询能力、`/clone/balance` 查询单价；余额不足再[充值](/app#developer?tab=recharge)。
3. 无需注册私有音色即可从 `/library/voices` 选取公共音色，参考下方完整示例保存第一份音频。

请求头使用 `X-API-Key`，也支持 `Authorization: Bearer <key>`。站内登录Token与API Key用途不同。Key通过 `HEYIN_API_KEY` 环境变量传入服务端程序，不放在URL、浏览器公开代码或日志里。

每笔业务固定保存参数和 `request_id`，网络重试使用原记录。示例中的业务文件包含请求参数，请按业务保存，不包含API Key；新业务换新文件名，已终态失败的异步任务重新提交需新ID。

## 接口参考
相对路径基于生产地址。所有账户、私有资源、合成、历史和任务接口均需API Key；只读 `/clone/catalog` 与 `/clone/catalog/voices` 无需Key，供选型和报价。

| 方法 | 路径 | 鉴权 | 功能 |
| --- | --- | --- | --- |
| GET | `/account` | API Key | 返回当前 Key 的只读账户、额度和计费信息。 |
| POST | `/clone/register` | API Key | 注册克隆音色；按当前Key的有效音色配额原子占位。 |
| POST | `/clone/synthesize` | API Key | 使用当前 API Key 名下的克隆音色合成语音。 |
| GET | `/clone/catalog` | 公开只读 | 无需Key的只读脱敏目录；不调用鉴权、更新最后调用时间或消耗积分。 |
| GET | `/clone/catalog/voices` | 公开只读 | 公开目录复用正式Key目录的可用性/公开模型/价格筛选，不泄露私有音色。 |
| GET | `/clone/voices` | API Key | 返回当前 API Key 名下注册的全部克隆音色。 |
| DELETE | `/clone/voices/{voice_id}` | API Key | 删除当前 API Key 名下的克隆音色（同时删除云侧声音）。 |
| GET | `/clone/models` | API Key | 返回可用克隆模型列表 + 情感指令预设（供客户端展示）。 |
| GET | `/clone/balance` | API Key | 返回当前 API Key 的预付费余额（三块拆分）、单价与充值包目录。 |
| GET | `/library/voices` | API Key | 分页浏览网页端当前可见、已审核通过的声音库音色。 |
| POST | `/library/synthesize` | API Key | 使用已审核、当前可见的公共音色合成，并按真实付费成本结算创作者收益。 |
| POST | `/clone/jobs` | API Key | 提交Key私有音色任务；提交阶段不扣费。 |
| POST | `/library/jobs` | API Key | 公共音色审核、上下架与余额在执行时重新检查。 |
| GET | `/jobs/{job_id}` | API Key | 仅返回当前Key任务，成功结果使用历史鉴权URL。 |
| GET | `/jobs/{job_id}/audio` | API Key | 同站点跳转，下载目标再次校验Key；不在URL里传密钥。 |
| GET | `/history` | API Key | 列出当前 API Key 最近 7 天的合成历史。 |
| GET | `/history/{history_id}/audio` | API Key | 按当前 Key 鉴权下载历史音频，支持二进制或 Base64。 |
| GET | `/timeline/capabilities` | API Key | 按当前Key查询时间线资格；节点即时状态由受理时重新检查。 |
| GET | `/history/{history_id}/timeline` | API Key | 实时读取时间线状态和公开文档，不把逐字数组放入合成回执。 |
| POST | `/history/{history_id}/alignment` | API Key | 已有成功音频补对齐或重试，不重新合成及扣费。 |
| GET | `/history/{history_id}/subtitles` | API Key | 下载四种字幕，JSON仅含公开协议字段。 |
| GET | `/history/{history_id}/audio-with-timeline` | API Key | 在独占临时目录导出副本，保持原文件与合成账单。 |

同步默认返回JSON中的 `audio_base64`，按Base64解码保存；公共音色用正整数 `shared_voice_id`，私有音色用本Key注册的 `voice_id`。站内私有音色不能直接作为Key的私有音色使用。

### 私有音色合成参数

| 字段 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- |
| `text` | 是 | string；最长 100000 | 合成文本 |
| `request_id` | 否 | string；最长 64；默认 "" | 幂等键（重试传同一值不会重复扣费） |
| `voice_id` | 是 | string | 克隆音色 ID（clone_xxx） |
| `rate` | 否 | number；最小 0.5；最大 2；默认 1 | 语速 0.5-2.0 |
| `instruction` | 否 | string；最长 200；默认 "" | 情感指令 |
| `language_type` | 否 | string；最长 20；默认 "Auto" | 语种 |
| `output_format` | 否 | string；可选 pcm / wav / mp3 / opus；默认 "wav" | 输出格式 pcm/wav/mp3/opus |
| `sample_rate` | 否 | integer；可选 24000 / 44100 / 48000；默认 24000 | 采样率 Hz（克隆音频最低 24kHz） |
| `response_format` | 否 | string；可选 base64 / url；默认 "base64" | 返回音频编码或鉴权下载地址 |
| `timeline_mode` | 否 | string；可选 off / native / auto / forced；默认 "off" | 可选时间线模式，默认关闭 |
| `pause_profile` | 否 | string；可选 general / audiobook / broadcast / ad；默认 "general" | 停顿预设：general/audiobook/broadcast/ad（见 ADR-0217） |

### 公共音色合成参数

| 字段 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- |
| `text` | 是 | string；最长 100000 | 合成文本 |
| `request_id` | 否 | string；最长 64；默认 "" | 可选幂等键 |
| `shared_voice_id` | 是 | integer | 声音库音色 ID |
| `rate` | 否 | number；最小 0.5；最大 2；默认 1 | 语速 0.5-2.0 |
| `instruction` | 否 | string；最长 200；默认 "" | 情感指令 |
| `language_type` | 否 | string；最长 20；默认 "Auto" | 语种 |
| `output_format` | 否 | string；可选 pcm / wav / mp3 / opus；默认 "wav" | 输出格式 |
| `sample_rate` | 否 | integer；可选 24000 / 44100 / 48000；默认 24000 | 采样率 Hz（克隆音频最低 24kHz） |
| `pause_profile` | 否 | string；可选 general / audiobook / broadcast / ad；默认 "general" |  |
| `response_format` | 否 | string；可选 base64 / url；默认 "base64" |  |
| `timeline_mode` | 否 | string；可选 off / native / auto / forced；默认 "off" |  |

`text` 请求模型原始最多100000字符，同步默认最多3000个非空白有效字，异步默认最多8000个有效字，服务端硬限32000；实际时间线上限见 `/timeline/capabilities`，同步配置另见 `/account.limits.max_synth_chars`。请求时间线时完整原文还须不超过32000个Unicode码点。英文限长按字符计算，计费另按规则折算。`instruction` 最长200字符，`language_type` 最长20字符，`request_id` 最长64字符。

注册采用multipart/form-data：参考音频建议3～30秒有效人声，1KB～50MB，整个请求仍受50MiB上限约束，须为表单留余量；不要自行设置缺少boundary的Content-Type。默认模型standard，实际需选择目录中 `registration_allowed=true` 且非维护的能力。注册费以余额接口为准，默认2000积分。

普通JSON成功响应如下；费用仅为说明字段的示例，实际以调用返回为准。
```json
{"success":true,"audio_base64":"...","format":"mp3","voice_name":"示例","cost":24,"remaining_points":976,"history_id":"hist_xxx","shared_voice_id":123,"price_per_char":6}
```
`shared_voice_id` 与 `price_per_char` 为公共音色响应字段。私有音色没有这两个字段；两者均返回费用、剩余积分及历史ID。音频格式为pcm/wav/mp3/opus；PCM为s16le裸音频，Opus请求44100Hz时转换为48000Hz输出。

## 使用示例
场景包括公共音色、私有音色注册、异步任务、鉴权下载、串行批量和错误重试。Node使用ESM的 `.mjs` 文件；Python需安装requests。Bash示例依赖curl 7.76+、jq、Python3与Linux base64；Windows使用PowerShell 7.2+版本。

所有示例都检查错误并设置超时。Node/Python在临时网络错误、429/502/503、同ID处理中409时最多尝试3次；参数冲突、鉴权或余额错误直接退出。Bash/PowerShell失败立即终止，按错误说明等待后保留原ID与参数重试。

### 公共音色合成

无需注册私有音色；目录没有音色时明确退出。

#### Node.js

Node.js 20+；保存为example.mjs，设置HEYIN_API_KEY后运行 node example.mjs。

```javascript
import { readFile, writeFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
const BASE = "https://www.heyintts.cn/api/open";
const KEY = process.env.HEYIN_API_KEY;
if (!KEY) throw new Error("请先设置HEYIN_API_KEY环境变量");
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const json = payload => ({method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});
async function business(file, payload) {
  try {
    const old = JSON.parse(await readFile(file, "utf8"));
    const {request_id, ...previous} = old;
    if (!request_id || JSON.stringify(previous) !== JSON.stringify(payload)) throw new Error("业务文件参数不一致，请为新业务使用新文件名");
    return old;
  } catch (error) {
    if (error.code !== "ENOENT") throw error;
    const value = {...payload, request_id:randomUUID()};
    await writeFile(file, JSON.stringify(value), {flag:"wx"}); return value;
  }
}
async function request(path, options = {}, binary = false) {
  const url = new URL(path.startsWith("/") && !path.startsWith("/api/") ? BASE + path : path, BASE);
  if (url.origin !== new URL(BASE).origin) throw new Error("拒绝向其他域发送API密钥");
  for (let attempt = 0; attempt < 3; attempt++) {
    let r;
    try { r = await fetch(url, {...options, headers:{"X-API-Key":KEY,...options.headers}, signal:AbortSignal.timeout(600000), redirect:"error"}); }
    catch (error) { if (attempt === 2) throw error; await sleep(1000 * 2 ** attempt); continue; }
    if (r.ok) { if(binary) return r; const data=await r.json(); if(data.success===false)throw new Error(data.error||"服务返回失败"); return data; }
    const message = await r.text();
    const temporary = [429,502,503].includes(r.status) || (r.status===409 && message.includes("正在处理中"));
    if (!temporary || attempt === 2) throw new Error("HTTP " + r.status + ": " + message);
    const retry = Number(r.headers.get("Retry-After"));
    await sleep(Math.min(60,Math.max(1,Number.isFinite(retry)&&retry>0?retry:2**attempt))*1000);
  }
}
async function save(data, file = "out.mp3") {
  const audio = data.audio_base64 ? Buffer.from(data.audio_base64,"base64")
    : Buffer.from(await (await request(data.audio_url, {}, true)).arrayBuffer());
  await writeFile(file, audio);
  console.log({file,cost:data.cost,remaining_points:data.remaining_points,history_id:data.history_id});
}
const { voices } = await request("/library/voices");
if (!voices?.length) throw new Error("当前没有可用的公共音色");
const payload = await business("public.json", { shared_voice_id: voices[0].shared_voice_id,
  text: "你好，欢迎使用合音。", output_format: "mp3" });
await save(await request("/library/synthesize", json(payload)));
```

#### Python

Python 3.10+；pip install requests；设置HEYIN_API_KEY后运行 python example.py。

```python
import os, uuid, json, base64, time
from pathlib import Path
from urllib.parse import urljoin, urlparse
import requests
BASE = "https://www.heyintts.cn/api/open"
KEY = os.environ.get("HEYIN_API_KEY")
if not KEY:
    raise RuntimeError("请先设置HEYIN_API_KEY环境变量")
def business(file, payload):
    p = Path(file)
    if p.exists():
        old = json.loads(p.read_text(encoding="utf-8"))
        if not old.get("request_id") or {k:v for k,v in old.items() if k!="request_id"} != payload:
            raise RuntimeError("业务文件参数不一致，请为新业务使用新文件名")
        return old
    value = dict(payload, request_id=str(uuid.uuid4()))
    with p.open("x", encoding="utf-8") as f:
        json.dump(value, f, ensure_ascii=False)
    return value
def request(method, path, **kwargs):
    url = urljoin(BASE, BASE+path if path.startswith("/") and not path.startswith("/api/") else path)
    if urlparse(url).netloc != urlparse(BASE).netloc or urlparse(url).scheme != "https":
        raise RuntimeError("拒绝向其他域发送API密钥")
    for attempt in range(3):
        # 上传重试前复位文件指针，确保参数和参考音频保持一致。
        for item in kwargs.get("files", {}).values():
            item[1].seek(0)
        try:
            r = requests.request(method, url, headers={"X-API-Key":KEY}, timeout=(15,600), allow_redirects=False, **kwargs)
        except (requests.Timeout, requests.ConnectionError):
            if attempt==2: raise
            time.sleep(2**attempt); continue
        if 200 <= r.status_code < 300:
            if "json" in r.headers.get("Content-Type", "") and r.json().get("success") is False:
                raise RuntimeError(r.json().get("error", "服务返回失败"))
            return r
        temporary = r.status_code in (429,502,503) or (r.status_code==409 and "正在处理中" in r.text)
        if not temporary or attempt==2:
            raise RuntimeError(f"HTTP {r.status_code}: {r.text}")
        retry = r.headers.get("Retry-After", "")
        time.sleep(min(60,max(1,int(retry) if retry.isdigit() else 2**attempt)))
def save(data, file="out.mp3"):
    audio = base64.b64decode(data["audio_base64"]) if data.get("audio_base64") else request("GET",data["audio_url"]).content
    Path(file).write_bytes(audio)
    print({k:data.get(k) for k in ("cost","remaining_points","history_id")}, "已保存", file)
voices = request("GET", "/library/voices").json()["voices"]
if not voices:
    raise RuntimeError("当前没有可用的公共音色")
payload = business("public.json", {"shared_voice_id": voices[0]["shared_voice_id"],
    "text": "你好，欢迎使用合音。", "output_format": "mp3"})
save(request("POST", "/library/synthesize", json=payload).json())
```

#### cURL · Bash

Linux Bash、curl 7.76+、jq、Python3；先 export HEYIN_API_KEY，再 bash example.sh。

```bash
set -euo pipefail
: "${HEYIN_API_KEY:?请先设置HEYIN_API_KEY}"
BASE="https://www.heyintts.cn/api/open"
STATE="heyin-public.request-id"
# 同一业务的重试保留此文件及参数，新业务换文件名。
if [ ! -f "$STATE" ]; then python3 -c 'import uuid;print(uuid.uuid4())' > "$STATE"; fi
ID=$(cat "$STATE")
req() { local endpoint="$1"; shift; curl --fail-with-body -sS --connect-timeout 15 --max-time 600 -H "X-API-Key: $HEYIN_API_KEY" "$BASE$endpoint" "$@"; }
save() {
 local data="$1" file="${2:-out.mp3}"
 if echo "$data" | jq -e '.audio_base64' >/dev/null; then
  echo "$data" | jq -r '.audio_base64' | base64 --decode > "$file"
 else
  local url; url=$(echo "$data" | jq -er '.audio_url')
  case "$url" in /api/open/history/*/audio) ;; *) echo "下载地址不符合预期" >&2; return 1;; esac
  curl --fail-with-body -sS --connect-timeout 15 --max-time 600 -H "X-API-Key: $HEYIN_API_KEY" "https://www.heyintts.cn$url" -o "$file"
 fi
 echo "$data" | jq '{cost,remaining_points,history_id}'
 echo "已保存 $file"
}
VOICE=$(req /library/voices | jq -er '.voices[0].shared_voice_id')
PAYLOAD=$(jq -nc --arg id "$ID" --argjson voice "$VOICE" '{shared_voice_id:$voice,text:"你好，欢迎使用合音。",output_format:"mp3",request_id:$id}')
# 429按Retry-After等待；重新执行时保留原ID和参数。
save "$(req /library/synthesize -H 'Content-Type: application/json' -d "$PAYLOAD")"

```

#### PowerShell

PowerShell 7.2+；先设置 $env:HEYIN_API_KEY，再执行 ./example.ps1。

```powershell
# PowerShell 7.2+，请先设置 $env:HEYIN_API_KEY。
$ErrorActionPreference = 'Stop'
if (-not $env:HEYIN_API_KEY) { throw '请先设置HEYIN_API_KEY' }
$base = 'https://www.heyintts.cn/api/open'
$headers = @{'X-API-Key'=$env:HEYIN_API_KEY}
$state = 'heyin-public.request-id'
if (-not (Test-Path -LiteralPath $state)) { [IO.File]::WriteAllText($state,[guid]::NewGuid().ToString()) }
$id = [IO.File]::ReadAllText($state).Trim()
function Req($endpoint,$payload=$null) {
 $args = @{Uri=($base+$endpoint);Headers=$headers;TimeoutSec=600;MaximumRedirection=0}
 if ($null -ne $payload) { $args.Method='Post';$args.ContentType='application/json; charset=utf-8';$args.Body=[Text.Encoding]::UTF8.GetBytes(($payload|ConvertTo-Json -Depth 10)) }
 Invoke-RestMethod @args
}
function Save-Audio($data,$file='out.mp3') {
 if ($data.audio_base64) { [IO.File]::WriteAllBytes($file,[Convert]::FromBase64String($data.audio_base64)) }
 else {
  $url=[uri]::new([uri]$base,[string]$data.audio_url)
  if ($url.Scheme -ne 'https' -or $url.Authority -ne ([uri]$base).Authority) { throw '下载地址不符合预期' }
  Invoke-WebRequest -Uri $url -Headers $headers -TimeoutSec 600 -MaximumRedirection 0 -OutFile $file
 }
 $data | Select-Object cost,remaining_points,history_id
 Write-Host "已保存 $file"
}
$voices = (Req '/library/voices').voices
if (-not $voices.Count) { throw '当前没有可用的公共音色' }
$payload = @{shared_voice_id=$voices[0].shared_voice_id;text='你好，欢迎使用合音。';output_format='mp3';request_id=$id}
# 失败时查看HTTP错误；429按Retry-After等待，重试保持同一ID和参数。
Save-Audio (Req '/library/synthesize' $payload)

```

### 私有音色注册与合成

准备3～30秒有效人声voice.mp3；先查询当前可注册能力，注册会计费。

#### Node.js

Node.js 20+；保存为example.mjs，设置HEYIN_API_KEY后运行 node example.mjs。

```javascript
import { readFile, writeFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
const BASE = "https://www.heyintts.cn/api/open";
const KEY = process.env.HEYIN_API_KEY;
if (!KEY) throw new Error("请先设置HEYIN_API_KEY环境变量");
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const json = payload => ({method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});
async function business(file, payload) {
  try {
    const old = JSON.parse(await readFile(file, "utf8"));
    const {request_id, ...previous} = old;
    if (!request_id || JSON.stringify(previous) !== JSON.stringify(payload)) throw new Error("业务文件参数不一致，请为新业务使用新文件名");
    return old;
  } catch (error) {
    if (error.code !== "ENOENT") throw error;
    const value = {...payload, request_id:randomUUID()};
    await writeFile(file, JSON.stringify(value), {flag:"wx"}); return value;
  }
}
async function request(path, options = {}, binary = false) {
  const url = new URL(path.startsWith("/") && !path.startsWith("/api/") ? BASE + path : path, BASE);
  if (url.origin !== new URL(BASE).origin) throw new Error("拒绝向其他域发送API密钥");
  for (let attempt = 0; attempt < 3; attempt++) {
    let r;
    try { r = await fetch(url, {...options, headers:{"X-API-Key":KEY,...options.headers}, signal:AbortSignal.timeout(600000), redirect:"error"}); }
    catch (error) { if (attempt === 2) throw error; await sleep(1000 * 2 ** attempt); continue; }
    if (r.ok) { if(binary) return r; const data=await r.json(); if(data.success===false)throw new Error(data.error||"服务返回失败"); return data; }
    const message = await r.text();
    const temporary = [429,502,503].includes(r.status) || (r.status===409 && message.includes("正在处理中"));
    if (!temporary || attempt === 2) throw new Error("HTTP " + r.status + ": " + message);
    const retry = Number(r.headers.get("Retry-After"));
    await sleep(Math.min(60,Math.max(1,Number.isFinite(retry)&&retry>0?retry:2**attempt))*1000);
  }
}
async function save(data, file = "out.mp3") {
  const audio = data.audio_base64 ? Buffer.from(data.audio_base64,"base64")
    : Buffer.from(await (await request(data.audio_url, {}, true)).arrayBuffer());
  await writeFile(file, audio);
  console.log({file,cost:data.cost,remaining_points:data.remaining_points,history_id:data.history_id});
}
const catalog = await request("/clone/models");
const model = catalog.models.find(m => m.registration_allowed && !m.maintenance);
if (!model) throw new Error("当前没有可注册的模型");
const registration = await business("register.json", { voice_name: "我的音色", model_key: model.key });
const form = new FormData();
form.append("file", new Blob([await readFile("voice.mp3")], {type:"audio/mpeg"}), "voice.mp3");
for (const [key, value] of Object.entries(registration)) form.append(key, String(value));
const voice = await request("/clone/register", { method: "POST", body: form });
const payload = await business("private.json", { voice_id: voice.voice_id,
  text: "你好，欢迎使用合音。", output_format: "mp3" });
await save(await request("/clone/synthesize", json(payload)));
```

#### Python

Python 3.10+；pip install requests；设置HEYIN_API_KEY后运行 python example.py。

```python
import os, uuid, json, base64, time
from pathlib import Path
from urllib.parse import urljoin, urlparse
import requests
BASE = "https://www.heyintts.cn/api/open"
KEY = os.environ.get("HEYIN_API_KEY")
if not KEY:
    raise RuntimeError("请先设置HEYIN_API_KEY环境变量")
def business(file, payload):
    p = Path(file)
    if p.exists():
        old = json.loads(p.read_text(encoding="utf-8"))
        if not old.get("request_id") or {k:v for k,v in old.items() if k!="request_id"} != payload:
            raise RuntimeError("业务文件参数不一致，请为新业务使用新文件名")
        return old
    value = dict(payload, request_id=str(uuid.uuid4()))
    with p.open("x", encoding="utf-8") as f:
        json.dump(value, f, ensure_ascii=False)
    return value
def request(method, path, **kwargs):
    url = urljoin(BASE, BASE+path if path.startswith("/") and not path.startswith("/api/") else path)
    if urlparse(url).netloc != urlparse(BASE).netloc or urlparse(url).scheme != "https":
        raise RuntimeError("拒绝向其他域发送API密钥")
    for attempt in range(3):
        # 上传重试前复位文件指针，确保参数和参考音频保持一致。
        for item in kwargs.get("files", {}).values():
            item[1].seek(0)
        try:
            r = requests.request(method, url, headers={"X-API-Key":KEY}, timeout=(15,600), allow_redirects=False, **kwargs)
        except (requests.Timeout, requests.ConnectionError):
            if attempt==2: raise
            time.sleep(2**attempt); continue
        if 200 <= r.status_code < 300:
            if "json" in r.headers.get("Content-Type", "") and r.json().get("success") is False:
                raise RuntimeError(r.json().get("error", "服务返回失败"))
            return r
        temporary = r.status_code in (429,502,503) or (r.status_code==409 and "正在处理中" in r.text)
        if not temporary or attempt==2:
            raise RuntimeError(f"HTTP {r.status_code}: {r.text}")
        retry = r.headers.get("Retry-After", "")
        time.sleep(min(60,max(1,int(retry) if retry.isdigit() else 2**attempt)))
def save(data, file="out.mp3"):
    audio = base64.b64decode(data["audio_base64"]) if data.get("audio_base64") else request("GET",data["audio_url"]).content
    Path(file).write_bytes(audio)
    print({k:data.get(k) for k in ("cost","remaining_points","history_id")}, "已保存", file)
catalog = request("GET", "/clone/models").json()
model = next((m for m in catalog["models"] if m["registration_allowed"] and not m["maintenance"]), None)
if not model:
    raise RuntimeError("当前没有可注册的模型")
registration = business("register.json", {"voice_name": "我的音色", "model_key": model["key"]})
with open("voice.mp3", "rb") as audio:
    voice = request("POST", "/clone/register", data=registration,
        files={"file": ("voice.mp3", audio, "audio/mpeg")}).json()
payload = business("private.json", {"voice_id": voice["voice_id"],
    "text": "你好，欢迎使用合音。", "output_format": "mp3"})
save(request("POST", "/clone/synthesize", json=payload).json())
```

#### cURL · Bash

Linux Bash、curl 7.76+、jq、Python3；先 export HEYIN_API_KEY，再 bash example.sh。

```bash
set -euo pipefail
: "${HEYIN_API_KEY:?请先设置HEYIN_API_KEY}"
BASE="https://www.heyintts.cn/api/open"
STATE="heyin-private.request-id"
# 同一业务的重试保留此文件及参数，新业务换文件名。
if [ ! -f "$STATE" ]; then python3 -c 'import uuid;print(uuid.uuid4())' > "$STATE"; fi
ID=$(cat "$STATE")
req() { local endpoint="$1"; shift; curl --fail-with-body -sS --connect-timeout 15 --max-time 600 -H "X-API-Key: $HEYIN_API_KEY" "$BASE$endpoint" "$@"; }
save() {
 local data="$1" file="${2:-out.mp3}"
 if echo "$data" | jq -e '.audio_base64' >/dev/null; then
  echo "$data" | jq -r '.audio_base64' | base64 --decode > "$file"
 else
  local url; url=$(echo "$data" | jq -er '.audio_url')
  case "$url" in /api/open/history/*/audio) ;; *) echo "下载地址不符合预期" >&2; return 1;; esac
  curl --fail-with-body -sS --connect-timeout 15 --max-time 600 -H "X-API-Key: $HEYIN_API_KEY" "https://www.heyintts.cn$url" -o "$file"
 fi
 echo "$data" | jq '{cost,remaining_points,history_id}'
 echo "已保存 $file"
}
MODEL=$(req /clone/models | jq -er '[.models[]|select(.registration_allowed and (.maintenance|not))][0].key')
VOICE=$(req /clone/register -F 'file=@voice.mp3;type=audio/mpeg' -F 'voice_name=我的音色' -F "model_key=$MODEL" -F "request_id=reg-$ID" | jq -er '.voice_id')
PAYLOAD=$(jq -nc --arg id "synth-$ID" --arg voice "$VOICE" '{voice_id:$voice,text:"你好",output_format:"mp3",request_id:$id}')
save "$(req /clone/synthesize -H 'Content-Type: application/json' -d "$PAYLOAD")"

```

#### PowerShell

PowerShell 7.2+；先设置 $env:HEYIN_API_KEY，再执行 ./example.ps1。

```powershell
# PowerShell 7.2+，请先设置 $env:HEYIN_API_KEY。
$ErrorActionPreference = 'Stop'
if (-not $env:HEYIN_API_KEY) { throw '请先设置HEYIN_API_KEY' }
$base = 'https://www.heyintts.cn/api/open'
$headers = @{'X-API-Key'=$env:HEYIN_API_KEY}
$state = 'heyin-private.request-id'
if (-not (Test-Path -LiteralPath $state)) { [IO.File]::WriteAllText($state,[guid]::NewGuid().ToString()) }
$id = [IO.File]::ReadAllText($state).Trim()
function Req($endpoint,$payload=$null) {
 $args = @{Uri=($base+$endpoint);Headers=$headers;TimeoutSec=600;MaximumRedirection=0}
 if ($null -ne $payload) { $args.Method='Post';$args.ContentType='application/json; charset=utf-8';$args.Body=[Text.Encoding]::UTF8.GetBytes(($payload|ConvertTo-Json -Depth 10)) }
 Invoke-RestMethod @args
}
function Save-Audio($data,$file='out.mp3') {
 if ($data.audio_base64) { [IO.File]::WriteAllBytes($file,[Convert]::FromBase64String($data.audio_base64)) }
 else {
  $url=[uri]::new([uri]$base,[string]$data.audio_url)
  if ($url.Scheme -ne 'https' -or $url.Authority -ne ([uri]$base).Authority) { throw '下载地址不符合预期' }
  Invoke-WebRequest -Uri $url -Headers $headers -TimeoutSec 600 -MaximumRedirection 0 -OutFile $file
 }
 $data | Select-Object cost,remaining_points,history_id
 Write-Host "已保存 $file"
}
$model = (Req '/clone/models').models | Where-Object { $_.registration_allowed -and -not $_.maintenance } | Select-Object -First 1
if (-not $model) { throw '没有可注册的模型' }
$voice = Invoke-RestMethod -Uri "$base/clone/register" -Method Post -Headers $headers -TimeoutSec 600 -Form @{file=(Get-Item -LiteralPath 'voice.mp3');voice_name='我的音色';model_key=$model.key;request_id="reg-$id"}
Save-Audio (Req '/clone/synthesize' @{voice_id=$voice.voice_id;text='你好';output_format='mp3';request_id="synth-$id"})

```

### 异步合成

提交后保存job_id；终态失败需修正原因并为新业务使用新的ID。

#### Node.js

Node.js 20+；保存为example.mjs，设置HEYIN_API_KEY后运行 node example.mjs。

```javascript
import { readFile, writeFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
const BASE = "https://www.heyintts.cn/api/open";
const KEY = process.env.HEYIN_API_KEY;
if (!KEY) throw new Error("请先设置HEYIN_API_KEY环境变量");
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const json = payload => ({method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});
async function business(file, payload) {
  try {
    const old = JSON.parse(await readFile(file, "utf8"));
    const {request_id, ...previous} = old;
    if (!request_id || JSON.stringify(previous) !== JSON.stringify(payload)) throw new Error("业务文件参数不一致，请为新业务使用新文件名");
    return old;
  } catch (error) {
    if (error.code !== "ENOENT") throw error;
    const value = {...payload, request_id:randomUUID()};
    await writeFile(file, JSON.stringify(value), {flag:"wx"}); return value;
  }
}
async function request(path, options = {}, binary = false) {
  const url = new URL(path.startsWith("/") && !path.startsWith("/api/") ? BASE + path : path, BASE);
  if (url.origin !== new URL(BASE).origin) throw new Error("拒绝向其他域发送API密钥");
  for (let attempt = 0; attempt < 3; attempt++) {
    let r;
    try { r = await fetch(url, {...options, headers:{"X-API-Key":KEY,...options.headers}, signal:AbortSignal.timeout(600000), redirect:"error"}); }
    catch (error) { if (attempt === 2) throw error; await sleep(1000 * 2 ** attempt); continue; }
    if (r.ok) { if(binary) return r; const data=await r.json(); if(data.success===false)throw new Error(data.error||"服务返回失败"); return data; }
    const message = await r.text();
    const temporary = [429,502,503].includes(r.status) || (r.status===409 && message.includes("正在处理中"));
    if (!temporary || attempt === 2) throw new Error("HTTP " + r.status + ": " + message);
    const retry = Number(r.headers.get("Retry-After"));
    await sleep(Math.min(60,Math.max(1,Number.isFinite(retry)&&retry>0?retry:2**attempt))*1000);
  }
}
async function save(data, file = "out.mp3") {
  const audio = data.audio_base64 ? Buffer.from(data.audio_base64,"base64")
    : Buffer.from(await (await request(data.audio_url, {}, true)).arrayBuffer());
  await writeFile(file, audio);
  console.log({file,cost:data.cost,remaining_points:data.remaining_points,history_id:data.history_id});
}
const { voices } = await request("/library/voices");
if (!voices?.length) throw new Error("当前没有可用的公共音色");
const payload = await business("job.json", { shared_voice_id: voices[0].shared_voice_id,
  text: "你好，欢迎使用异步合成。", output_format: "mp3" });
let job = await request("/library/jobs", json(payload));
const deadline = Date.now() + 600000;
while (["queued", "processing"].includes(job.status)) {
  if (Date.now() > deadline) throw new Error("等待超时，请保存job_id后继续查询：" + job.job_id);
  await sleep(2000); job = await request("/jobs/" + job.job_id);
}
if (job.status !== "succeeded") throw new Error(JSON.stringify(job.error) + "；修正后使用新的业务文件提交");
await save(job.result);
```

#### Python

Python 3.10+；pip install requests；设置HEYIN_API_KEY后运行 python example.py。

```python
import os, uuid, json, base64, time
from pathlib import Path
from urllib.parse import urljoin, urlparse
import requests
BASE = "https://www.heyintts.cn/api/open"
KEY = os.environ.get("HEYIN_API_KEY")
if not KEY:
    raise RuntimeError("请先设置HEYIN_API_KEY环境变量")
def business(file, payload):
    p = Path(file)
    if p.exists():
        old = json.loads(p.read_text(encoding="utf-8"))
        if not old.get("request_id") or {k:v for k,v in old.items() if k!="request_id"} != payload:
            raise RuntimeError("业务文件参数不一致，请为新业务使用新文件名")
        return old
    value = dict(payload, request_id=str(uuid.uuid4()))
    with p.open("x", encoding="utf-8") as f:
        json.dump(value, f, ensure_ascii=False)
    return value
def request(method, path, **kwargs):
    url = urljoin(BASE, BASE+path if path.startswith("/") and not path.startswith("/api/") else path)
    if urlparse(url).netloc != urlparse(BASE).netloc or urlparse(url).scheme != "https":
        raise RuntimeError("拒绝向其他域发送API密钥")
    for attempt in range(3):
        # 上传重试前复位文件指针，确保参数和参考音频保持一致。
        for item in kwargs.get("files", {}).values():
            item[1].seek(0)
        try:
            r = requests.request(method, url, headers={"X-API-Key":KEY}, timeout=(15,600), allow_redirects=False, **kwargs)
        except (requests.Timeout, requests.ConnectionError):
            if attempt==2: raise
            time.sleep(2**attempt); continue
        if 200 <= r.status_code < 300:
            if "json" in r.headers.get("Content-Type", "") and r.json().get("success") is False:
                raise RuntimeError(r.json().get("error", "服务返回失败"))
            return r
        temporary = r.status_code in (429,502,503) or (r.status_code==409 and "正在处理中" in r.text)
        if not temporary or attempt==2:
            raise RuntimeError(f"HTTP {r.status_code}: {r.text}")
        retry = r.headers.get("Retry-After", "")
        time.sleep(min(60,max(1,int(retry) if retry.isdigit() else 2**attempt)))
def save(data, file="out.mp3"):
    audio = base64.b64decode(data["audio_base64"]) if data.get("audio_base64") else request("GET",data["audio_url"]).content
    Path(file).write_bytes(audio)
    print({k:data.get(k) for k in ("cost","remaining_points","history_id")}, "已保存", file)
voices = request("GET", "/library/voices").json()["voices"]
if not voices:
    raise RuntimeError("当前没有可用的公共音色")
payload = business("job.json", {"shared_voice_id": voices[0]["shared_voice_id"],
    "text": "你好，欢迎使用异步合成。", "output_format": "mp3"})
job = request("POST", "/library/jobs", json=payload).json()
deadline = time.monotonic() + 600
while job["status"] in ("queued", "processing"):
    if time.monotonic() > deadline:
        raise RuntimeError("等待超时，请保存job_id后继续查询：" + job["job_id"])
    time.sleep(2)
    job = request("GET", "/jobs/" + job["job_id"]).json()
if job["status"] != "succeeded":
    raise RuntimeError(str(job["error"]) + "；修正后使用新的业务文件提交")
save(job["result"])
```

#### cURL · Bash

Linux Bash、curl 7.76+、jq、Python3；先 export HEYIN_API_KEY，再 bash example.sh。

```bash
set -euo pipefail
: "${HEYIN_API_KEY:?请先设置HEYIN_API_KEY}"
BASE="https://www.heyintts.cn/api/open"
STATE="heyin-jobs.request-id"
# 同一业务的重试保留此文件及参数，新业务换文件名。
if [ ! -f "$STATE" ]; then python3 -c 'import uuid;print(uuid.uuid4())' > "$STATE"; fi
ID=$(cat "$STATE")
req() { local endpoint="$1"; shift; curl --fail-with-body -sS --connect-timeout 15 --max-time 600 -H "X-API-Key: $HEYIN_API_KEY" "$BASE$endpoint" "$@"; }
save() {
 local data="$1" file="${2:-out.mp3}"
 if echo "$data" | jq -e '.audio_base64' >/dev/null; then
  echo "$data" | jq -r '.audio_base64' | base64 --decode > "$file"
 else
  local url; url=$(echo "$data" | jq -er '.audio_url')
  case "$url" in /api/open/history/*/audio) ;; *) echo "下载地址不符合预期" >&2; return 1;; esac
  curl --fail-with-body -sS --connect-timeout 15 --max-time 600 -H "X-API-Key: $HEYIN_API_KEY" "https://www.heyintts.cn$url" -o "$file"
 fi
 echo "$data" | jq '{cost,remaining_points,history_id}'
 echo "已保存 $file"
}
VOICE=$(req /library/voices | jq -er '.voices[0].shared_voice_id')
PAYLOAD=$(jq -nc --arg id "$ID" --argjson voice "$VOICE" '{shared_voice_id:$voice,text:"你好，欢迎使用合音。",output_format:"mp3",request_id:$id}')
JOB=$(req /library/jobs -H 'Content-Type: application/json' -d "$PAYLOAD")
DEADLINE=$((SECONDS+600))
while echo "$JOB" | jq -e '.status=="queued" or .status=="processing"' >/dev/null; do
 [ "$SECONDS" -lt "$DEADLINE" ] || { echo "$JOB" >&2; exit 1; }
 sleep 2; JOB=$(req "/jobs/$(echo "$JOB" | jq -r '.job_id')")
done
echo "$JOB" | jq -e '.status=="succeeded"' >/dev/null || { echo "$JOB" >&2; exit 1; }
save "$(echo "$JOB" | jq -c '.result')"

```

#### PowerShell

PowerShell 7.2+；先设置 $env:HEYIN_API_KEY，再执行 ./example.ps1。

```powershell
# PowerShell 7.2+，请先设置 $env:HEYIN_API_KEY。
$ErrorActionPreference = 'Stop'
if (-not $env:HEYIN_API_KEY) { throw '请先设置HEYIN_API_KEY' }
$base = 'https://www.heyintts.cn/api/open'
$headers = @{'X-API-Key'=$env:HEYIN_API_KEY}
$state = 'heyin-jobs.request-id'
if (-not (Test-Path -LiteralPath $state)) { [IO.File]::WriteAllText($state,[guid]::NewGuid().ToString()) }
$id = [IO.File]::ReadAllText($state).Trim()
function Req($endpoint,$payload=$null) {
 $args = @{Uri=($base+$endpoint);Headers=$headers;TimeoutSec=600;MaximumRedirection=0}
 if ($null -ne $payload) { $args.Method='Post';$args.ContentType='application/json; charset=utf-8';$args.Body=[Text.Encoding]::UTF8.GetBytes(($payload|ConvertTo-Json -Depth 10)) }
 Invoke-RestMethod @args
}
function Save-Audio($data,$file='out.mp3') {
 if ($data.audio_base64) { [IO.File]::WriteAllBytes($file,[Convert]::FromBase64String($data.audio_base64)) }
 else {
  $url=[uri]::new([uri]$base,[string]$data.audio_url)
  if ($url.Scheme -ne 'https' -or $url.Authority -ne ([uri]$base).Authority) { throw '下载地址不符合预期' }
  Invoke-WebRequest -Uri $url -Headers $headers -TimeoutSec 600 -MaximumRedirection 0 -OutFile $file
 }
 $data | Select-Object cost,remaining_points,history_id
 Write-Host "已保存 $file"
}
$voices = (Req '/library/voices').voices
if (-not $voices.Count) { throw '当前没有可用的公共音色' }
$payload = @{shared_voice_id=$voices[0].shared_voice_id;text='你好，欢迎使用合音。';output_format='mp3';request_id=$id}
$job = Req '/library/jobs' $payload
$deadline = [DateTime]::UtcNow.AddMinutes(10)
while ($job.status -in @('queued','processing')) {
 if ([DateTime]::UtcNow -gt $deadline) { throw "等待超时，请继续查询任务 $($job.job_id)" }
 Start-Sleep -Seconds 2; $job = Req ("/jobs/"+$job.job_id)
}
if ($job.status -ne 'succeeded') { throw ($job.error|ConvertTo-Json) }
Save-Audio $job.result

```

### 鉴权下载

下载地址携带同Key鉴权，直接读取历史地址，避免跨域重定向丢失鉴权。

#### Node.js

Node.js 20+；保存为example.mjs，设置HEYIN_API_KEY后运行 node example.mjs。

```javascript
import { readFile, writeFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
const BASE = "https://www.heyintts.cn/api/open";
const KEY = process.env.HEYIN_API_KEY;
if (!KEY) throw new Error("请先设置HEYIN_API_KEY环境变量");
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const json = payload => ({method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});
async function business(file, payload) {
  try {
    const old = JSON.parse(await readFile(file, "utf8"));
    const {request_id, ...previous} = old;
    if (!request_id || JSON.stringify(previous) !== JSON.stringify(payload)) throw new Error("业务文件参数不一致，请为新业务使用新文件名");
    return old;
  } catch (error) {
    if (error.code !== "ENOENT") throw error;
    const value = {...payload, request_id:randomUUID()};
    await writeFile(file, JSON.stringify(value), {flag:"wx"}); return value;
  }
}
async function request(path, options = {}, binary = false) {
  const url = new URL(path.startsWith("/") && !path.startsWith("/api/") ? BASE + path : path, BASE);
  if (url.origin !== new URL(BASE).origin) throw new Error("拒绝向其他域发送API密钥");
  for (let attempt = 0; attempt < 3; attempt++) {
    let r;
    try { r = await fetch(url, {...options, headers:{"X-API-Key":KEY,...options.headers}, signal:AbortSignal.timeout(600000), redirect:"error"}); }
    catch (error) { if (attempt === 2) throw error; await sleep(1000 * 2 ** attempt); continue; }
    if (r.ok) { if(binary) return r; const data=await r.json(); if(data.success===false)throw new Error(data.error||"服务返回失败"); return data; }
    const message = await r.text();
    const temporary = [429,502,503].includes(r.status) || (r.status===409 && message.includes("正在处理中"));
    if (!temporary || attempt === 2) throw new Error("HTTP " + r.status + ": " + message);
    const retry = Number(r.headers.get("Retry-After"));
    await sleep(Math.min(60,Math.max(1,Number.isFinite(retry)&&retry>0?retry:2**attempt))*1000);
  }
}
async function save(data, file = "out.mp3") {
  const audio = data.audio_base64 ? Buffer.from(data.audio_base64,"base64")
    : Buffer.from(await (await request(data.audio_url, {}, true)).arrayBuffer());
  await writeFile(file, audio);
  console.log({file,cost:data.cost,remaining_points:data.remaining_points,history_id:data.history_id});
}
const { voices } = await request("/library/voices");
if (!voices?.length) throw new Error("当前没有可用的公共音色");
const payload = await business("url.json", { shared_voice_id: voices[0].shared_voice_id,
  text: "这段音频通过鉴权地址下载。", output_format: "mp3", response_format: "url" });
await save(await request("/library/synthesize", json(payload)));
```

#### Python

Python 3.10+；pip install requests；设置HEYIN_API_KEY后运行 python example.py。

```python
import os, uuid, json, base64, time
from pathlib import Path
from urllib.parse import urljoin, urlparse
import requests
BASE = "https://www.heyintts.cn/api/open"
KEY = os.environ.get("HEYIN_API_KEY")
if not KEY:
    raise RuntimeError("请先设置HEYIN_API_KEY环境变量")
def business(file, payload):
    p = Path(file)
    if p.exists():
        old = json.loads(p.read_text(encoding="utf-8"))
        if not old.get("request_id") or {k:v for k,v in old.items() if k!="request_id"} != payload:
            raise RuntimeError("业务文件参数不一致，请为新业务使用新文件名")
        return old
    value = dict(payload, request_id=str(uuid.uuid4()))
    with p.open("x", encoding="utf-8") as f:
        json.dump(value, f, ensure_ascii=False)
    return value
def request(method, path, **kwargs):
    url = urljoin(BASE, BASE+path if path.startswith("/") and not path.startswith("/api/") else path)
    if urlparse(url).netloc != urlparse(BASE).netloc or urlparse(url).scheme != "https":
        raise RuntimeError("拒绝向其他域发送API密钥")
    for attempt in range(3):
        # 上传重试前复位文件指针，确保参数和参考音频保持一致。
        for item in kwargs.get("files", {}).values():
            item[1].seek(0)
        try:
            r = requests.request(method, url, headers={"X-API-Key":KEY}, timeout=(15,600), allow_redirects=False, **kwargs)
        except (requests.Timeout, requests.ConnectionError):
            if attempt==2: raise
            time.sleep(2**attempt); continue
        if 200 <= r.status_code < 300:
            if "json" in r.headers.get("Content-Type", "") and r.json().get("success") is False:
                raise RuntimeError(r.json().get("error", "服务返回失败"))
            return r
        temporary = r.status_code in (429,502,503) or (r.status_code==409 and "正在处理中" in r.text)
        if not temporary or attempt==2:
            raise RuntimeError(f"HTTP {r.status_code}: {r.text}")
        retry = r.headers.get("Retry-After", "")
        time.sleep(min(60,max(1,int(retry) if retry.isdigit() else 2**attempt)))
def save(data, file="out.mp3"):
    audio = base64.b64decode(data["audio_base64"]) if data.get("audio_base64") else request("GET",data["audio_url"]).content
    Path(file).write_bytes(audio)
    print({k:data.get(k) for k in ("cost","remaining_points","history_id")}, "已保存", file)
voices = request("GET", "/library/voices").json()["voices"]
if not voices:
    raise RuntimeError("当前没有可用的公共音色")
payload = business("url.json", {"shared_voice_id": voices[0]["shared_voice_id"],
    "text": "这段音频通过鉴权地址下载。", "output_format": "mp3", "response_format": "url"})
save(request("POST", "/library/synthesize", json=payload).json())
```

#### cURL · Bash

Linux Bash、curl 7.76+、jq、Python3；先 export HEYIN_API_KEY，再 bash example.sh。

```bash
set -euo pipefail
: "${HEYIN_API_KEY:?请先设置HEYIN_API_KEY}"
BASE="https://www.heyintts.cn/api/open"
STATE="heyin-url.request-id"
# 同一业务的重试保留此文件及参数，新业务换文件名。
if [ ! -f "$STATE" ]; then python3 -c 'import uuid;print(uuid.uuid4())' > "$STATE"; fi
ID=$(cat "$STATE")
req() { local endpoint="$1"; shift; curl --fail-with-body -sS --connect-timeout 15 --max-time 600 -H "X-API-Key: $HEYIN_API_KEY" "$BASE$endpoint" "$@"; }
save() {
 local data="$1" file="${2:-out.mp3}"
 if echo "$data" | jq -e '.audio_base64' >/dev/null; then
  echo "$data" | jq -r '.audio_base64' | base64 --decode > "$file"
 else
  local url; url=$(echo "$data" | jq -er '.audio_url')
  case "$url" in /api/open/history/*/audio) ;; *) echo "下载地址不符合预期" >&2; return 1;; esac
  curl --fail-with-body -sS --connect-timeout 15 --max-time 600 -H "X-API-Key: $HEYIN_API_KEY" "https://www.heyintts.cn$url" -o "$file"
 fi
 echo "$data" | jq '{cost,remaining_points,history_id}'
 echo "已保存 $file"
}
VOICE=$(req /library/voices | jq -er '.voices[0].shared_voice_id')
PAYLOAD=$(jq -nc --arg id "$ID" --argjson voice "$VOICE" '{shared_voice_id:$voice,text:"你好，欢迎使用合音。",output_format:"mp3",request_id:$id}')
PAYLOAD=$(echo "$PAYLOAD" | jq -c '.response_format="url"')
# 429按Retry-After等待；重新执行时保留原ID和参数。
save "$(req /library/synthesize -H 'Content-Type: application/json' -d "$PAYLOAD")"

```

#### PowerShell

PowerShell 7.2+；先设置 $env:HEYIN_API_KEY，再执行 ./example.ps1。

```powershell
# PowerShell 7.2+，请先设置 $env:HEYIN_API_KEY。
$ErrorActionPreference = 'Stop'
if (-not $env:HEYIN_API_KEY) { throw '请先设置HEYIN_API_KEY' }
$base = 'https://www.heyintts.cn/api/open'
$headers = @{'X-API-Key'=$env:HEYIN_API_KEY}
$state = 'heyin-url.request-id'
if (-not (Test-Path -LiteralPath $state)) { [IO.File]::WriteAllText($state,[guid]::NewGuid().ToString()) }
$id = [IO.File]::ReadAllText($state).Trim()
function Req($endpoint,$payload=$null) {
 $args = @{Uri=($base+$endpoint);Headers=$headers;TimeoutSec=600;MaximumRedirection=0}
 if ($null -ne $payload) { $args.Method='Post';$args.ContentType='application/json; charset=utf-8';$args.Body=[Text.Encoding]::UTF8.GetBytes(($payload|ConvertTo-Json -Depth 10)) }
 Invoke-RestMethod @args
}
function Save-Audio($data,$file='out.mp3') {
 if ($data.audio_base64) { [IO.File]::WriteAllBytes($file,[Convert]::FromBase64String($data.audio_base64)) }
 else {
  $url=[uri]::new([uri]$base,[string]$data.audio_url)
  if ($url.Scheme -ne 'https' -or $url.Authority -ne ([uri]$base).Authority) { throw '下载地址不符合预期' }
  Invoke-WebRequest -Uri $url -Headers $headers -TimeoutSec 600 -MaximumRedirection 0 -OutFile $file
 }
 $data | Select-Object cost,remaining_points,history_id
 Write-Host "已保存 $file"
}
$voices = (Req '/library/voices').voices
if (-not $voices.Count) { throw '当前没有可用的公共音色' }
$payload = @{shared_voice_id=$voices[0].shared_voice_id;text='你好，欢迎使用合音。';output_format='mp3';request_id=$id}
$payload.response_format='url'
# 失败时查看HTTP错误；429按Retry-After等待，重试保持同一ID和参数。
Save-Audio (Req '/library/synthesize' $payload)

```

### 串行批量合成

逐项保存业务ID与结果，控制并发；再次执行复用原业务记录。

#### Node.js

Node.js 20+；保存为example.mjs，设置HEYIN_API_KEY后运行 node example.mjs。

```javascript
import { readFile, writeFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
const BASE = "https://www.heyintts.cn/api/open";
const KEY = process.env.HEYIN_API_KEY;
if (!KEY) throw new Error("请先设置HEYIN_API_KEY环境变量");
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const json = payload => ({method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});
async function business(file, payload) {
  try {
    const old = JSON.parse(await readFile(file, "utf8"));
    const {request_id, ...previous} = old;
    if (!request_id || JSON.stringify(previous) !== JSON.stringify(payload)) throw new Error("业务文件参数不一致，请为新业务使用新文件名");
    return old;
  } catch (error) {
    if (error.code !== "ENOENT") throw error;
    const value = {...payload, request_id:randomUUID()};
    await writeFile(file, JSON.stringify(value), {flag:"wx"}); return value;
  }
}
async function request(path, options = {}, binary = false) {
  const url = new URL(path.startsWith("/") && !path.startsWith("/api/") ? BASE + path : path, BASE);
  if (url.origin !== new URL(BASE).origin) throw new Error("拒绝向其他域发送API密钥");
  for (let attempt = 0; attempt < 3; attempt++) {
    let r;
    try { r = await fetch(url, {...options, headers:{"X-API-Key":KEY,...options.headers}, signal:AbortSignal.timeout(600000), redirect:"error"}); }
    catch (error) { if (attempt === 2) throw error; await sleep(1000 * 2 ** attempt); continue; }
    if (r.ok) { if(binary) return r; const data=await r.json(); if(data.success===false)throw new Error(data.error||"服务返回失败"); return data; }
    const message = await r.text();
    const temporary = [429,502,503].includes(r.status) || (r.status===409 && message.includes("正在处理中"));
    if (!temporary || attempt === 2) throw new Error("HTTP " + r.status + ": " + message);
    const retry = Number(r.headers.get("Retry-After"));
    await sleep(Math.min(60,Math.max(1,Number.isFinite(retry)&&retry>0?retry:2**attempt))*1000);
  }
}
async function save(data, file = "out.mp3") {
  const audio = data.audio_base64 ? Buffer.from(data.audio_base64,"base64")
    : Buffer.from(await (await request(data.audio_url, {}, true)).arrayBuffer());
  await writeFile(file, audio);
  console.log({file,cost:data.cost,remaining_points:data.remaining_points,history_id:data.history_id});
}
const { voices } = await request("/library/voices");
if (!voices?.length) throw new Error("当前没有可用的公共音色");
const texts = ["这是第一段配音。", "这是第二段配音。"];
// 串行执行，避免超出Key并发额度；重启脚本会复用每一项的业务记录。
for (const [index, text] of texts.entries()) {
  const payload = await business("batch-" + index + ".json", {
    shared_voice_id: voices[0].shared_voice_id, text, output_format: "mp3" });
  await save(await request("/library/synthesize", json(payload)), "out-" + index + ".mp3");
}
```

#### Python

Python 3.10+；pip install requests；设置HEYIN_API_KEY后运行 python example.py。

```python
import os, uuid, json, base64, time
from pathlib import Path
from urllib.parse import urljoin, urlparse
import requests
BASE = "https://www.heyintts.cn/api/open"
KEY = os.environ.get("HEYIN_API_KEY")
if not KEY:
    raise RuntimeError("请先设置HEYIN_API_KEY环境变量")
def business(file, payload):
    p = Path(file)
    if p.exists():
        old = json.loads(p.read_text(encoding="utf-8"))
        if not old.get("request_id") or {k:v for k,v in old.items() if k!="request_id"} != payload:
            raise RuntimeError("业务文件参数不一致，请为新业务使用新文件名")
        return old
    value = dict(payload, request_id=str(uuid.uuid4()))
    with p.open("x", encoding="utf-8") as f:
        json.dump(value, f, ensure_ascii=False)
    return value
def request(method, path, **kwargs):
    url = urljoin(BASE, BASE+path if path.startswith("/") and not path.startswith("/api/") else path)
    if urlparse(url).netloc != urlparse(BASE).netloc or urlparse(url).scheme != "https":
        raise RuntimeError("拒绝向其他域发送API密钥")
    for attempt in range(3):
        # 上传重试前复位文件指针，确保参数和参考音频保持一致。
        for item in kwargs.get("files", {}).values():
            item[1].seek(0)
        try:
            r = requests.request(method, url, headers={"X-API-Key":KEY}, timeout=(15,600), allow_redirects=False, **kwargs)
        except (requests.Timeout, requests.ConnectionError):
            if attempt==2: raise
            time.sleep(2**attempt); continue
        if 200 <= r.status_code < 300:
            if "json" in r.headers.get("Content-Type", "") and r.json().get("success") is False:
                raise RuntimeError(r.json().get("error", "服务返回失败"))
            return r
        temporary = r.status_code in (429,502,503) or (r.status_code==409 and "正在处理中" in r.text)
        if not temporary or attempt==2:
            raise RuntimeError(f"HTTP {r.status_code}: {r.text}")
        retry = r.headers.get("Retry-After", "")
        time.sleep(min(60,max(1,int(retry) if retry.isdigit() else 2**attempt)))
def save(data, file="out.mp3"):
    audio = base64.b64decode(data["audio_base64"]) if data.get("audio_base64") else request("GET",data["audio_url"]).content
    Path(file).write_bytes(audio)
    print({k:data.get(k) for k in ("cost","remaining_points","history_id")}, "已保存", file)
voices = request("GET", "/library/voices").json()["voices"]
if not voices:
    raise RuntimeError("当前没有可用的公共音色")
# 串行执行；重启脚本会复用每项的业务记录与幂等ID。
for index, text in enumerate(["这是第一段配音。", "这是第二段配音。"]):
    payload = business(f"batch-{index}.json", {"shared_voice_id": voices[0]["shared_voice_id"],
        "text": text, "output_format": "mp3"})
    save(request("POST", "/library/synthesize", json=payload).json(), f"out-{index}.mp3")
```

#### cURL · Bash

Linux Bash、curl 7.76+、jq、Python3；先 export HEYIN_API_KEY，再 bash example.sh。

```bash
set -euo pipefail
: "${HEYIN_API_KEY:?请先设置HEYIN_API_KEY}"
BASE="https://www.heyintts.cn/api/open"
STATE="heyin-batch.request-id"
# 同一业务的重试保留此文件及参数，新业务换文件名。
if [ ! -f "$STATE" ]; then python3 -c 'import uuid;print(uuid.uuid4())' > "$STATE"; fi
ID=$(cat "$STATE")
req() { local endpoint="$1"; shift; curl --fail-with-body -sS --connect-timeout 15 --max-time 600 -H "X-API-Key: $HEYIN_API_KEY" "$BASE$endpoint" "$@"; }
save() {
 local data="$1" file="${2:-out.mp3}"
 if echo "$data" | jq -e '.audio_base64' >/dev/null; then
  echo "$data" | jq -r '.audio_base64' | base64 --decode > "$file"
 else
  local url; url=$(echo "$data" | jq -er '.audio_url')
  case "$url" in /api/open/history/*/audio) ;; *) echo "下载地址不符合预期" >&2; return 1;; esac
  curl --fail-with-body -sS --connect-timeout 15 --max-time 600 -H "X-API-Key: $HEYIN_API_KEY" "https://www.heyintts.cn$url" -o "$file"
 fi
 echo "$data" | jq '{cost,remaining_points,history_id}'
 echo "已保存 $file"
}
VOICE=$(req /library/voices | jq -er '.voices[0].shared_voice_id')
PAYLOAD=$(jq -nc --arg id "$ID" --argjson voice "$VOICE" '{shared_voice_id:$voice,text:"你好，欢迎使用合音。",output_format:"mp3",request_id:$id}')
for i in 0 1; do
 ITEM=$(echo "$PAYLOAD" | jq -c --arg id "$ID-$i" --arg text "这是第$i段配音。" '.request_id=$id|.text=$text')
 save "$(req /library/synthesize -H 'Content-Type: application/json' -d "$ITEM")" "out-$i.mp3"
done

```

#### PowerShell

PowerShell 7.2+；先设置 $env:HEYIN_API_KEY，再执行 ./example.ps1。

```powershell
# PowerShell 7.2+，请先设置 $env:HEYIN_API_KEY。
$ErrorActionPreference = 'Stop'
if (-not $env:HEYIN_API_KEY) { throw '请先设置HEYIN_API_KEY' }
$base = 'https://www.heyintts.cn/api/open'
$headers = @{'X-API-Key'=$env:HEYIN_API_KEY}
$state = 'heyin-batch.request-id'
if (-not (Test-Path -LiteralPath $state)) { [IO.File]::WriteAllText($state,[guid]::NewGuid().ToString()) }
$id = [IO.File]::ReadAllText($state).Trim()
function Req($endpoint,$payload=$null) {
 $args = @{Uri=($base+$endpoint);Headers=$headers;TimeoutSec=600;MaximumRedirection=0}
 if ($null -ne $payload) { $args.Method='Post';$args.ContentType='application/json; charset=utf-8';$args.Body=[Text.Encoding]::UTF8.GetBytes(($payload|ConvertTo-Json -Depth 10)) }
 Invoke-RestMethod @args
}
function Save-Audio($data,$file='out.mp3') {
 if ($data.audio_base64) { [IO.File]::WriteAllBytes($file,[Convert]::FromBase64String($data.audio_base64)) }
 else {
  $url=[uri]::new([uri]$base,[string]$data.audio_url)
  if ($url.Scheme -ne 'https' -or $url.Authority -ne ([uri]$base).Authority) { throw '下载地址不符合预期' }
  Invoke-WebRequest -Uri $url -Headers $headers -TimeoutSec 600 -MaximumRedirection 0 -OutFile $file
 }
 $data | Select-Object cost,remaining_points,history_id
 Write-Host "已保存 $file"
}
$voices = (Req '/library/voices').voices
if (-not $voices.Count) { throw '当前没有可用的公共音色' }
$payload = @{shared_voice_id=$voices[0].shared_voice_id;text='你好，欢迎使用合音。';output_format='mp3';request_id=$id}
for ($i=0;$i -lt 2;$i++) {
 $payload.request_id="$id-$i";$payload.text="这是第 $i 段配音。"
 Save-Audio (Req '/library/synthesize' $payload) "out-$i.mp3"
}

```

### 错误与安全重试

401/402/参数冲突立即提示；Node/Python仅对临时错误有限退避。

#### Node.js

Node.js 20+；保存为example.mjs，设置HEYIN_API_KEY后运行 node example.mjs。

```javascript
import { readFile, writeFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
const BASE = "https://www.heyintts.cn/api/open";
const KEY = process.env.HEYIN_API_KEY;
if (!KEY) throw new Error("请先设置HEYIN_API_KEY环境变量");
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const json = payload => ({method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});
async function business(file, payload) {
  try {
    const old = JSON.parse(await readFile(file, "utf8"));
    const {request_id, ...previous} = old;
    if (!request_id || JSON.stringify(previous) !== JSON.stringify(payload)) throw new Error("业务文件参数不一致，请为新业务使用新文件名");
    return old;
  } catch (error) {
    if (error.code !== "ENOENT") throw error;
    const value = {...payload, request_id:randomUUID()};
    await writeFile(file, JSON.stringify(value), {flag:"wx"}); return value;
  }
}
async function request(path, options = {}, binary = false) {
  const url = new URL(path.startsWith("/") && !path.startsWith("/api/") ? BASE + path : path, BASE);
  if (url.origin !== new URL(BASE).origin) throw new Error("拒绝向其他域发送API密钥");
  for (let attempt = 0; attempt < 3; attempt++) {
    let r;
    try { r = await fetch(url, {...options, headers:{"X-API-Key":KEY,...options.headers}, signal:AbortSignal.timeout(600000), redirect:"error"}); }
    catch (error) { if (attempt === 2) throw error; await sleep(1000 * 2 ** attempt); continue; }
    if (r.ok) { if(binary) return r; const data=await r.json(); if(data.success===false)throw new Error(data.error||"服务返回失败"); return data; }
    const message = await r.text();
    const temporary = [429,502,503].includes(r.status) || (r.status===409 && message.includes("正在处理中"));
    if (!temporary || attempt === 2) throw new Error("HTTP " + r.status + ": " + message);
    const retry = Number(r.headers.get("Retry-After"));
    await sleep(Math.min(60,Math.max(1,Number.isFinite(retry)&&retry>0?retry:2**attempt))*1000);
  }
}
async function save(data, file = "out.mp3") {
  const audio = data.audio_base64 ? Buffer.from(data.audio_base64,"base64")
    : Buffer.from(await (await request(data.audio_url, {}, true)).arrayBuffer());
  await writeFile(file, audio);
  console.log({file,cost:data.cost,remaining_points:data.remaining_points,history_id:data.history_id});
}
// request已区分鉴权/余额/参数错误，只对临时错误做有限退避。
// 业务文件保存原始参数与request_id，网络超时后再次运行仍复用同一笔请求。
const { voices } = await request("/library/voices");
if (!voices?.length) throw new Error("当前没有可用的公共音色");
const payload = await business("retry.json", { shared_voice_id: voices[0].shared_voice_id,
  text: "网络中断后使用相同业务记录重试。", output_format: "mp3" });
await save(await request("/library/synthesize", json(payload)));
```

#### Python

Python 3.10+；pip install requests；设置HEYIN_API_KEY后运行 python example.py。

```python
import os, uuid, json, base64, time
from pathlib import Path
from urllib.parse import urljoin, urlparse
import requests
BASE = "https://www.heyintts.cn/api/open"
KEY = os.environ.get("HEYIN_API_KEY")
if not KEY:
    raise RuntimeError("请先设置HEYIN_API_KEY环境变量")
def business(file, payload):
    p = Path(file)
    if p.exists():
        old = json.loads(p.read_text(encoding="utf-8"))
        if not old.get("request_id") or {k:v for k,v in old.items() if k!="request_id"} != payload:
            raise RuntimeError("业务文件参数不一致，请为新业务使用新文件名")
        return old
    value = dict(payload, request_id=str(uuid.uuid4()))
    with p.open("x", encoding="utf-8") as f:
        json.dump(value, f, ensure_ascii=False)
    return value
def request(method, path, **kwargs):
    url = urljoin(BASE, BASE+path if path.startswith("/") and not path.startswith("/api/") else path)
    if urlparse(url).netloc != urlparse(BASE).netloc or urlparse(url).scheme != "https":
        raise RuntimeError("拒绝向其他域发送API密钥")
    for attempt in range(3):
        # 上传重试前复位文件指针，确保参数和参考音频保持一致。
        for item in kwargs.get("files", {}).values():
            item[1].seek(0)
        try:
            r = requests.request(method, url, headers={"X-API-Key":KEY}, timeout=(15,600), allow_redirects=False, **kwargs)
        except (requests.Timeout, requests.ConnectionError):
            if attempt==2: raise
            time.sleep(2**attempt); continue
        if 200 <= r.status_code < 300:
            if "json" in r.headers.get("Content-Type", "") and r.json().get("success") is False:
                raise RuntimeError(r.json().get("error", "服务返回失败"))
            return r
        temporary = r.status_code in (429,502,503) or (r.status_code==409 and "正在处理中" in r.text)
        if not temporary or attempt==2:
            raise RuntimeError(f"HTTP {r.status_code}: {r.text}")
        retry = r.headers.get("Retry-After", "")
        time.sleep(min(60,max(1,int(retry) if retry.isdigit() else 2**attempt)))
def save(data, file="out.mp3"):
    audio = base64.b64decode(data["audio_base64"]) if data.get("audio_base64") else request("GET",data["audio_url"]).content
    Path(file).write_bytes(audio)
    print({k:data.get(k) for k in ("cost","remaining_points","history_id")}, "已保存", file)
# request仅对临时错误退避；401/402/参数冲突立即显示服务端错误。
# 业务文件保存原始参数与request_id，网络超时后再次运行仍复用同一笔请求。
voices = request("GET", "/library/voices").json()["voices"]
if not voices:
    raise RuntimeError("当前没有可用的公共音色")
payload = business("retry.json", {"shared_voice_id": voices[0]["shared_voice_id"],
    "text": "网络中断后使用相同业务记录重试。", "output_format": "mp3"})
save(request("POST", "/library/synthesize", json=payload).json())
```

#### cURL · Bash

Linux Bash、curl 7.76+、jq、Python3；先 export HEYIN_API_KEY，再 bash example.sh。

```bash
set -euo pipefail
: "${HEYIN_API_KEY:?请先设置HEYIN_API_KEY}"
BASE="https://www.heyintts.cn/api/open"
STATE="heyin-retry.request-id"
# 同一业务的重试保留此文件及参数，新业务换文件名。
if [ ! -f "$STATE" ]; then python3 -c 'import uuid;print(uuid.uuid4())' > "$STATE"; fi
ID=$(cat "$STATE")
req() { local endpoint="$1"; shift; curl --fail-with-body -sS --connect-timeout 15 --max-time 600 -H "X-API-Key: $HEYIN_API_KEY" "$BASE$endpoint" "$@"; }
save() {
 local data="$1" file="${2:-out.mp3}"
 if echo "$data" | jq -e '.audio_base64' >/dev/null; then
  echo "$data" | jq -r '.audio_base64' | base64 --decode > "$file"
 else
  local url; url=$(echo "$data" | jq -er '.audio_url')
  case "$url" in /api/open/history/*/audio) ;; *) echo "下载地址不符合预期" >&2; return 1;; esac
  curl --fail-with-body -sS --connect-timeout 15 --max-time 600 -H "X-API-Key: $HEYIN_API_KEY" "https://www.heyintts.cn$url" -o "$file"
 fi
 echo "$data" | jq '{cost,remaining_points,history_id}'
 echo "已保存 $file"
}
VOICE=$(req /library/voices | jq -er '.voices[0].shared_voice_id')
PAYLOAD=$(jq -nc --arg id "$ID" --argjson voice "$VOICE" '{shared_voice_id:$voice,text:"你好，欢迎使用合音。",output_format:"mp3",request_id:$id}')
# 429按Retry-After等待；重新执行时保留原ID和参数。
save "$(req /library/synthesize -H 'Content-Type: application/json' -d "$PAYLOAD")"

```

#### PowerShell

PowerShell 7.2+；先设置 $env:HEYIN_API_KEY，再执行 ./example.ps1。

```powershell
# PowerShell 7.2+，请先设置 $env:HEYIN_API_KEY。
$ErrorActionPreference = 'Stop'
if (-not $env:HEYIN_API_KEY) { throw '请先设置HEYIN_API_KEY' }
$base = 'https://www.heyintts.cn/api/open'
$headers = @{'X-API-Key'=$env:HEYIN_API_KEY}
$state = 'heyin-retry.request-id'
if (-not (Test-Path -LiteralPath $state)) { [IO.File]::WriteAllText($state,[guid]::NewGuid().ToString()) }
$id = [IO.File]::ReadAllText($state).Trim()
function Req($endpoint,$payload=$null) {
 $args = @{Uri=($base+$endpoint);Headers=$headers;TimeoutSec=600;MaximumRedirection=0}
 if ($null -ne $payload) { $args.Method='Post';$args.ContentType='application/json; charset=utf-8';$args.Body=[Text.Encoding]::UTF8.GetBytes(($payload|ConvertTo-Json -Depth 10)) }
 Invoke-RestMethod @args
}
function Save-Audio($data,$file='out.mp3') {
 if ($data.audio_base64) { [IO.File]::WriteAllBytes($file,[Convert]::FromBase64String($data.audio_base64)) }
 else {
  $url=[uri]::new([uri]$base,[string]$data.audio_url)
  if ($url.Scheme -ne 'https' -or $url.Authority -ne ([uri]$base).Authority) { throw '下载地址不符合预期' }
  Invoke-WebRequest -Uri $url -Headers $headers -TimeoutSec 600 -MaximumRedirection 0 -OutFile $file
 }
 $data | Select-Object cost,remaining_points,history_id
 Write-Host "已保存 $file"
}
$voices = (Req '/library/voices').voices
if (-not $voices.Count) { throw '当前没有可用的公共音色' }
$payload = @{shared_voice_id=$voices[0].shared_voice_id;text='你好，欢迎使用合音。';output_format='mp3';request_id=$id}
# 失败时查看HTTP错误；429按Retry-After等待，重试保持同一ID和参数。
Save-Audio (Req '/library/synthesize' $payload)

```

## 账户、公共音色与历史
`GET /account` 返回当前Key名称、状态、余额、音色配额、单次字数及限流配置。只读查询免费，仍计入Key请求限额。

`GET /library/voices` 返回已审核、可见、未下架、未删除、未过期且支持开放API的公共音色。支持 `page/page_size`（默认1/20，最多50）、`keyword`（最长200）、`category`、`lang`、`gender`、`age_group`、`scene/scene_key`、`model`、`grade`。scene_key优先，model使用公开能力代号。

返回 `voices`、`total`、`page`、`page_size`；音色包含 `shared_voice_id`、`name`、`avatar_url`、`description`、`model`、`price_per_char`、`category`、`language`、`gender`、`age_group`、`scenes`、`quality_grade`、`is_new_voice`、`creator_name`、`total_usage`、`preview_url`。公共音色不会复制到私有列表；合成时重新校验可用性及价格。本人使用自己的公共音色仍计费，但不产生创作者收益。

`GET /clone/voices` 返回当前Key的私有音色，包含voice_id、voice_name、model_key、status、created_at、language、expires_at、expired、visibility，兼容保留lang、expire_at、last_synth_at、is_expired。

`GET /history?page=1&page_size=20&status=succeeded` 返回当前Key最近7天进入执行阶段的记录；status可为空、processing、succeeded、failed。前置鉴权、参数或审核拒绝不保证产生历史。每条包含history_id、request_id、type、展示信息、char_count、cost、status、output_format、sample_rate、created_at、has_audio及失败信息。

## 异步合成与鉴权下载
将对应同步参数提交到 `POST /clone/jobs` 或 `POST /library/jobs`，返回HTTP202，保存job_id。提交只排队不扣费，执行时检查音色、文本、余额和当前Key状态。结果固定为URL模式。

```json
{"success":true,"job_id":"job_xxx","status":"queued","created_at":1793318400,"expires_at":1793923200,"result":null,"error":null}
```

每2秒查询 `GET /jobs/{job_id}`；状态为queued、processing、succeeded或failed。成功时result包含费用、history_id和audio_url；失败时error包含status_code与message。每Key最多5个未完成任务，平台队列默认100个；查询也计入请求限额。任务与音频历史各保留7天，任务终态后清理正文。

同Key、同request_id和同参数重复提交返回原任务，参数不同返回409。终态failed不会因重复提交同ID重新执行；修正原因后使用新的业务ID。未提供ID时每次提交均为新任务。

`response_format=url` 的同步结果不返回audio_base64，而返回形如 `/api/open/history/hist_xxx/audio` 的audio_url。以站点源拼接地址，避免重复拼接/api/open，下载仍携带同Key。该地址不是公开播放链接。

`GET /history/{history_id}/audio` 默认返回二进制，传 `response_format=base64` 返回JSON。文件清理后410，不存在、非本Key或非成功记录404。历史查询本身只展示保留窗口内记录，不能依赖永久留存。

`GET /jobs/{job_id}/audio` 成功时307跳转至同站点历史接口，未成功409。客户端必须保留鉴权头；示例直接下载result.audio_url，避免不同HTTP库的重定向鉴权差异。

## 配额与生命周期
每Key默认最多100个有效私有音色，实际配额见/account。已删除、过期音色不占配额。注册后或最近一次成功合成后，连续30天无成功合成才进入清理流程；每次成功合成刷新计时。主动删除立即释放配额。

账号默认最多5个有效Key，以控制台显示为准。Key一旦吊销立即失效，无法继续查询、合成或下载；重建Key会产生新的资源归属，不代表自动迁移旧音色和历史。

## 模型目录与情感控制
公开能力代号包括standard、balanced、emotion、pro、v3及兼容项turbo、premium；V5开放后使用v5。可用性、注册资格、价格、语言及维护状态实时查询 `/clone/models`；未创建Key可浏览 `/clone/catalog`。不根据历史日期硬编码注册资格。

目录包含catalog_version、models、compat_models；每项包含key、name、cost_per_char、preset_mode、emotion_support、language_support、sample_rates、formats、supports_rate、max_chars、registration_allowed、maintenance和inline_tag_support。带Key的模型目录还包含presets。

`preset_mode=instruction` 支持自由风格指令；`preset_mode=tag` 从当前目录选择正文语气标签，不等同于任意自然语言指令。inline_tag_support.tags提供tag、label、category、default；单次最多100个标签。目录内合法标签不朗读、不参与限长和计费；V5自由提示的计费见下文。

正文可使用 `[pause:1.5]`：每处0.3～5秒，最多8处，合计不超过30秒；合法停顿不朗读、不参与限长及计费，纯停顿正文不可合成。pause_profile为general/audiobook/broadcast/ad，默认general。


<a id="apidocs-v5"></a>
### HEYIN Natural V5 接入

V5 对外代号为 `v5`，以当前Key的 `/clone/models` 返回为准。公开目录未列出时不代表灰度Key已获得权限，未授权调用返回403。功能默认关闭，开放范围与注册资格均由目录和实际调用校验。

私有音色注册沿用 `POST /clone/register`，multipart表单传 `model_key=v5`；合成使用注册返回的本Key `voice_id`，不在合成时更换模型。公共音色可用 `GET /library/voices?model=v5` 筛选，再将 `shared_voice_id` 传给原 `/library/synthesize` 或 `/library/jobs`。站内私有音色不能直接用于第三方Key。

表演提示可写在正文中，例如 `[带着笑意说]你好，欢迎使用合音。`。能力目录的 `inline_tag_support.mode=freeform` 表示自由提示，`direction_presets` 提供内置快捷提示。提示不可为空、嵌套、跨行或缺括号，最多100处；必须有实际台词。停顿继续使用 `[pause:1.5]`。

当前V5基础单价为30积分/计费字，始终以模型报价与合成回执为准；公共音色使用其 `price_per_char`。目录内语气标签和明确匹配的内置提示按现行规则免计费，自定义自由提示不一律免计费。网页估算不执行生成，最终费用以 `cost` 为准。V5继续沿用API余额、幂等重试、失败退款、异步任务和鉴权下载，不自动迁移已有音色或改变旧模型费用。

V5不额外承诺原生时间线；需要字幕时先查询时间线资格和模式。下载地址只返回合音鉴权地址，不返回第三方服务地址或云侧音色标识。

已获V5权限的Key可使用以下注册示例；先设置环境变量 `HEYIN_API_KEY`，不要把真实Key写入代码或URL。失败时先检查HTTP状态与错误内容，不要使用未成功响应中的音色ID。
```bash
curl --fail-with-body --max-time 180 \
  -H "X-API-Key: $HEYIN_API_KEY" \
  -F 'file=@voice.wav' -F 'voice_name=我的音色' \
  -F 'model_key=v5' -F 'prompt_lang=zh' -F 'denoise=false' \
  -F 'request_id=v5-register-business-001' \
  'https://www.heyintts.cn/api/open/clone/register'
```
随后以返回的 `voice_id` 替换下方占位值，将JSON提交至 `/clone/synthesize`。长文本可提交 `/clone/jobs` 并按异步章节轮询与下载；新的业务使用新的request_id，网络重试则保留原ID与参数。
```json
{"voice_id":"clone_返回的业务音色ID","text":"[带着笑意说]你好，欢迎使用合音。","request_id":"v5-synth-business-001","output_format":"mp3","response_format":"url"}
```

## API定价与费用估算
预付费按量计费，无固定Free订阅套餐。单价以 `/clone/balance`、`/clone/models` 和公共音色目录price_per_char为准。充值目录与页面共用后端公开报价；注册、同步和异步实际执行均以调用时校验为准。

计费规则：中文、数字和其他字母每个计1字；ASCII英文字母每2个计1字、奇数向上取整；标点符号另每2个计1字、奇数向上取整；空白不计费。合法停顿和当前能力目录语气标签先剥离。`你好[pause:1.5]世界` 为4个计费字，单价6时24积分。

基础兑换率为1元=10000积分；5积分/字时1000计费字为5000积分、约0.50元，6积分/字时为6000积分、约0.60元，均未计赠送折扣。估算不发起合成，最终费用以cost为准。

充值包/赠送以目录packs为准；按量充值最低10元、金额为10的倍数。API专用余额默认按积分包→赠送→按量余额扣减；已绑定统一账户的Key按实际账户规则扣减。控制台用量为累计净消耗，不是当月额度，退款计入净额。

## 时间线与字幕下载
合成可选 `timeline_mode`，默认 `off`。支持 `native` 原生时间戳、`auto` 优先原生并按Key资格异步对齐、`forced` 外置对齐。请求前查询 `GET /timeline/capabilities?model=emotion` 获取当前Key资格及同步/异步上限；只支持公开模型代号。功能尚未启用返回503；forced无资格403、节点离线503、队列满429，均在合成扣费前拒绝。

`auto` 无外置资格、节点不可用或准入队列满时回退原生；没有可验证结果返回unavailable。原生可能是词级，查看granularity；要求中文逐字可选forced。运行中节点掉线仍可能失败，不保证受理即完成逐字对齐。

当前外置节点只开放中文/英文，见external_languages；其它明确语种或可识别的其它文字，forced在扣费前422拒绝，auto回退原生。供应商原生模式的语种继续由音色能力决定。

```json
{"shared_voice_id":123,"text":"你好，欢迎使用。","request_id":"timeline-demo-001","output_format":"mp3","response_format":"url","timeline_mode":"auto"}
```

音频成功即可使用，响应增加timeline_status、timeline_url和timeline_poll_after_ms；保持原cost、remaining_points与音频字段。状态为pending/processing/ready/partial/failed/unavailable。按建议间隔轮询，429遵守Retry-After；GET /history/{history_id}/timeline返回最新状态及可用文档。异步合成jobs在音频完成时succeeded，时间线可继续pending，后续任务查询反映最新时间线状态。

四种字幕在 `GET /history/{history_id}/subtitles?format=json` 下载，format还支持srt/vtt/ass。JSON含完整逐字/词时间；SRT/VTT按句输出，ASS支持字词时序。`GET /history/{history_id}/audio-with-timeline?format=auto` 返回内嵌MP3/WAV副本；format=wav请求WAV，Opus/已知采样率PCM经解码校验后可封装WAV。原音频不变；常规播放器不保证展示自定义内嵌字幕，需要支持协议的客户端解析。

查询文档沿用schema_version=1，时间为全音频整数毫秒。text_start/text_end针对返回文档text，为Unicode码点左闭右开索引；服务端会去首尾空白并规范化合法标签，不应直接索引未处理的请求原字符串。JavaScript须按码点转换，不能直接套UTF-16下标。quality包含text_coverage和missing_text_ranges；无法可靠定位的字保持partial，不能按字数均分时间。partial导出需显式allow_partial=true，否则409。

已有音频可 `POST /history/{history_id}/alignment`，请求体 `{}`；失败或partial时传 `{"retry":true}` 重试。需要服务端保留的完整原文和可信历史关联，旧版历史缺关联返回422；旧长音频缺可信分段也返回422，不猜时间或重新合成。单资源最多三次申请/重试，间隔至少30秒，超过429。对齐不重新扣取合成费用；更改朗读文字或声音需要新音频版本。

所有查询和下载均需原Key鉴权，同账号其它Key无权读取。资源过期410、版本变化409、未知或异Key404，Key吊销401。下载地址不含Key，不应作为公开分享链接。沿用历史7天保留期，请及时保存音频及字幕；重试、状态查询与导出仍计入Key请求限流。

默认off保持旧幂等摘要；显式时间线模式参与参数摘要，同ID改变模式409。已有音频补对齐走alignment接口，避免改合成参数后重复计费。时间线独立失败不会回退已成功音频或再次合成；检查timeline_error_code与retryable处理。

[下载完整Python时间线示例](/examples/openapi-timeline.py)：安装requests，设置HEYIN_API_KEY及HEYIN_SHARED_VOICE_ID后运行。示例固定保存业务参数和request_id，先下载原音频再等待时间线；重复运行同一输出目录复用原任务，新业务使用新的HEYIN_OUTPUT_DIR。示例需要已开放该能力的Key，默认只接受HTTPS同源下载。

## 错误处理与安全重试
| HTTP | 处理方式 |
| --- | --- |
| 400 | 检查文本、停顿、音色配额和能力资格；字数错误含TTS_TEXT_TOO_LONG |
| 401 | Key缺失、无效或已吊销；检查环境变量与请求头 |
| 402 | 余额不足或消费额度不足；核对账户后处理 |
| 403 | 能力不允许新注册或内容不可对外使用 |
| 404 | 资源不存在、无权限或已经过期 |
| 409 | 参数冲突、同ID处理中或任务尚未成功，分别处理 |
| 410 | 原结果已清理，核对业务后使用新ID；旧任务提交亦可能返回 |
| 413 | HTTP请求体过大，压缩参考音频并为表单留余量 |
| 422 | 字段类型、范围、枚举或内容/样本校验失败 |
| 429 | 请求限流、排队已满或等待超时，按Retry-After退避 |
| 500/502/503 | 合成失败、维护或暂时不可用；已扣费用失败自动退款，异常由补偿继续处理 |

常见错误detail是字符串、包含code/message的对象，或字段校验数组，必须先检查HTTP状态。不要在错误响应上读取audio_base64。
```json
{"detail":{"code":"API_BUSY","message":"请求较多，请稍后重试"}}
```

同步建议固定request_id：同Key同接口同参数重放原成功结果，不重复扣费；参数不同409，仍处理中409。失败退款后可复用原同步ID重试。不同同步接口不能复用同ID；幂等记录默认7天，超期无去重保证，业务侧保存订单与音频结果。

默认每Key每10秒20次请求，查询、轮询和下载也计数。云调用跨worker共享全局及每Key上限，有限排队；具体配置通过/account读取。429按Retry-After退避；网络超时先复用原参数和ID，不立即换ID重新扣费。异步终态失败的重提规则见异步章节。
