声岛开放平台

曲库 API

把声岛的正版曲库接到你的 App、网站或小程序里。你的用户可以直接搜歌、试听,下载后即可用于作品。

请求示例
GET /open/v1/song/search?keyword=吉他
X-App-Key: sdak_••••••••

{
  "code": 0,
  "data": {
    "records": [{
      "songName": "温暖民谣吉他vlog配乐5",
      "performer": "董谷",
      "bpm": 96
    }]
  }
}
搜索曲库
按关键词、风格标签、BPM、时长筛选歌曲。
浏览歌单
直接使用我们按场景整理好的歌单。
在线试听
拿到完整音频,在你的产品里直接播放。
下载使用
下载 MP3 用于视频、直播等作品。

概述

接口包括曲库搜索、歌单、试听和下载。所有请求都要按「签名」一节的方法签名。

接入地址https://api.musicisle.cn,下文记作 BASE_URL。仅支持 HTTPS。
路径前缀/open/v1
编码UTF-8。POST 请求体为 JSON,Content-Type: application/json; charset=utf-8。
时间时间戳一律为 Unix 秒。

返回格式

{
  "code": 0,              // 0 为成功,其他见「错误码」
  "msg": "成功",
  "data": { ... },        // 业务数据,失败时为 null
  "requestId": "9f1c..."  // 请求ID,反馈问题时请提供
}

失败时 HTTP 状态码同时会反映错误类别(如 400、401、403、429、500),code 给出具体原因。响应头 X-Request-Id 与 requestId 相同。

返回字段没有值时为 null,字段本身不会省略。除各类 ID 外,字段都可能为 null,请做好兼容。各类 ID 均为字符串,请勿按数字处理。

内容范围

接口只返回当前上架、可正常使用的歌曲;歌曲下架后,搜索、详情和歌单中都不再出现,下载接口也会返回 40400。

接入流程

1
联系开通
和商务确认合作方式后,我们会把 AppKey 和 AppSecret 发给你。
2
对接接口
在你的服务端按「签名」说明发起请求,可以先用「校验用例」确认签名算对了。
3
上线
如果约定了 IP 白名单,请从白名单里的服务器调用。
4
查看用量和费用
用绑定的手机号登录官网,在控制台查看调用量、下载记录和费用。

签名

请求头

请求头说明
X-App-Key你的 AppKey
X-Timestamp当前 Unix 时间(秒)。与服务器时间相差超过 300 秒会被拒绝,请保持服务器时钟同步。
X-Nonce随机字符串,建议 32 位十六进制。300 秒内不能重复使用,每次请求(包括重试)都要重新生成。
X-Signature签名值,计算方法见下文

待签字符串与签名

按以下顺序用换行符 \n 拼接 6 段(最后一段后面没有换行):

HTTP方法大写
请求路径
规范化查询串
X-Timestamp 的值
X-Nonce 的值
请求体的 SHA-256(小写十六进制)

签名 = HMAC-SHA256(AppSecret, 待签字符串),结果转为小写十六进制,放入 X-Signature。

段落规则
HTTP 方法GET 或 POST
请求路径以 /open/v1 开头的路径,不含域名和查询串,如 /open/v1/song/search
规范化查询串把每个查询参数写成 参数名=参数值,使用 URL 编码之前的原文;同名参数(如多个 tagIds)各自成一条;所有条目按字符串字典序排序后用 & 连接。没有查询参数时为空字符串(这一行为空,但换行符仍要保留)。
请求体摘要对实际发送的请求体字节计算 SHA-256。没有请求体(GET)时为空内容的摘要:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
常见签名错误:签名用的 JSON 和实际发送的不是同一串字节(例如签名后又重新序列化了一次);查询串用了编码后的值参与签名;漏掉了空查询串那一行。

校验用例

可用下面的固定输入检验自己的实现,签名结果应与用例一致。

AppSecretdemoSecret0123456789abcdefghijklmnopqrstuvwxyzAB
X-Timestamp1761456000
X-Nonce5f2c9a7e1b3d4c6f8a0e2d4b6c8f0a1e

用例 1:GET /open/v1/song/search?keyword=%E9%92%A2%E7%90%B4&tagIds=t002&tagIds=t001&page=1

待签字符串:
GET
/open/v1/song/search
keyword=钢琴&page=1&tagIds=t001&tagIds=t002
1761456000
5f2c9a7e1b3d4c6f8a0e2d4b6c8f0a1e
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

X-Signature:9678d7f703671ada7f9236c7bd71ece26b14fd8a764152a16a416383de71a4f6

用例 2:POST /open/v1/song/download,请求体(UTF-8,无多余空格)如下

{"songId":"1790000000000000001","userName":"张三"}

请求体 SHA-256:8de8527088f753a802e928e44cc3761e51aabb4a91ae42262d271834eedb2ab8
待签字符串:
POST
/open/v1/song/download
                    ← 空查询串,这一行为空
1761456000
5f2c9a7e1b3d4c6f8a0e2d4b6c8f0a1e
8de8527088f753a802e928e44cc3761e51aabb4a91ae42262d271834eedb2ab8

X-Signature:03def1dae89ec27b78d17e9f3c707ceaaa99d32b427b0dad8d44ffce6e712987

示例代码

完整可运行的签名与调用示例,替换 AppKey 与 AppSecret 即可使用。

Java 8+,无第三方依赖
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.*;

public class OpenApiClient {

    private final String baseUrl;
    private final String appKey;
    private final String appSecret;
    private final SecureRandom random = new SecureRandom();

    public OpenApiClient(String baseUrl, String appKey, String appSecret) {
        this.baseUrl = baseUrl;
        this.appKey = appKey;
        this.appSecret = appSecret;
    }

    /** GET 请求。同名参数(如多个 tagIds)放进同一个 List */
    public String get(String path, Map<String, List<String>> query) throws Exception {
        return send("GET", path, query, new byte[0]);
    }

    /** POST 请求,body 为 JSON 字符串 */
    public String post(String path, String jsonBody) throws Exception {
        return send("POST", path, Collections.emptyMap(), jsonBody.getBytes(StandardCharsets.UTF_8));
    }

    private String send(String method, String path, Map<String, List<String>> query, byte[] body) throws Exception {
        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
        String nonce = randomHex(16);
        String signature = sign(appSecret, method, path, query, body, timestamp, nonce);

        URL url = new URL(baseUrl + path + encodeQuery(query));
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod(method);
        conn.setConnectTimeout(5000);
        conn.setReadTimeout(15000);
        conn.setRequestProperty("X-App-Key", appKey);
        conn.setRequestProperty("X-Timestamp", timestamp);
        conn.setRequestProperty("X-Nonce", nonce);
        conn.setRequestProperty("X-Signature", signature);
        if (body.length > 0) {
            conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
            conn.setDoOutput(true);
            try (OutputStream out = conn.getOutputStream()) {
                out.write(body);
            }
        }
        int status = conn.getResponseCode();
        InputStream in = status < 400 ? conn.getInputStream() : conn.getErrorStream();
        return readAll(in);
    }

    public static String sign(String secret, String method, String path, Map<String, List<String>> query,
                              byte[] body, String timestamp, String nonce) throws Exception {
        String stringToSign = method.toUpperCase() + "\n"
                + path + "\n"
                + canonicalQuery(query) + "\n"
                + timestamp + "\n"
                + nonce + "\n"
                + sha256Hex(body);
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        return toHex(mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)));
    }

    /** 未编码的 name=value 逐条拼好后整体按字典序排序,再用 & 连接 */
    static String canonicalQuery(Map<String, List<String>> query) {
        List<String> pairs = new ArrayList<>();
        for (Map.Entry<String, List<String>> entry : query.entrySet()) {
            for (String value : entry.getValue()) {
                pairs.add(entry.getKey() + "=" + value);
            }
        }
        Collections.sort(pairs);
        return String.join("&", pairs);
    }

    private static String encodeQuery(Map<String, List<String>> query) throws Exception {
        List<String> pairs = new ArrayList<>();
        for (Map.Entry<String, List<String>> entry : query.entrySet()) {
            for (String value : entry.getValue()) {
                pairs.add(URLEncoder.encode(entry.getKey(), "UTF-8") + "=" + URLEncoder.encode(value, "UTF-8"));
            }
        }
        return pairs.isEmpty() ? "" : "?" + String.join("&", pairs);
    }

    private static String sha256Hex(byte[] content) throws Exception {
        return toHex(MessageDigest.getInstance("SHA-256").digest(content));
    }

    private static String toHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    private String randomHex(int byteCount) {
        byte[] bytes = new byte[byteCount];
        random.nextBytes(bytes);
        return toHex(bytes);
    }

    private static String readAll(InputStream in) throws Exception {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        int n;
        while ((n = in.read(buffer)) != -1) {
            out.write(buffer, 0, n);
        }
        return new String(out.toByteArray(), StandardCharsets.UTF_8);
    }

    public static void main(String[] args) throws Exception {
        OpenApiClient client = new OpenApiClient("https://api.musicisle.cn", "你的AppKey", "你的AppSecret");

        Map<String, List<String>> query = new LinkedHashMap<>();
        query.put("keyword", Collections.singletonList("钢琴"));
        query.put("tagIds", Arrays.asList("t001", "t002"));
        query.put("page", Collections.singletonList("1"));
        System.out.println(client.get("/open/v1/song/search", query));

        System.out.println(client.post("/open/v1/song/download",
                "{\"songId\":\"1790000000000000001\",\"userName\":\"张三\"}"));
    }
}

接口

「接口编码」用于开通权限和用量统计;未开通的接口会返回 40301。

接口方法与路径接口编码
标签树GET /open/v1/tag/treetag.tree
筛选项字典GET /open/v1/filtersfilters
歌曲搜索GET /open/v1/song/searchsong.search
歌曲详情GET /open/v1/song/detailsong.detail
歌单列表GET /open/v1/playlist/listplaylist.list
歌单详情GET /open/v1/playlist/detailplaylist.detail
下载歌曲POST /open/v1/song/downloadsong.download
试听歌曲GET /open/v1/song/playsong.play
接口返回的封面、波形、歌词、音频地址都带签名,30 分钟内有效。请在需要时实时获取,不要长期保存这些地址。

分页结构

带分页的接口在 data(或其中的列表字段)里返回:

字段类型说明
pageint当前页,从 1 开始
pageSizeint每页条数,固定值
totallong命中总条数
totalPagesint可翻页数,已按该接口的最大页数截断
hasMoreboolean是否还有下一页
recordsarray当前页数据
接口每页条数最多翻页
歌曲搜索2010
歌单列表102
歌单详情中的歌曲2010

超过最大页数会返回 40001,请通过更精确的筛选条件缩小结果。实际值以「筛选项字典」接口的 pageRules 为准。

GET/open/v1/tag/tree标签树tag.tree

无参数。data 是一级标签数组,每个节点用 children 嵌套下一级标签,目前共三级。

返回字段(每个节点)
字段类型说明
tagIdstring标签ID,搜索时填入 tagIds
tagNamestring标签名称
tagLevelint层级:1 一级、2 二级、3 三级
firstTagIdstring所属一级标签ID,即搜索时的分组依据;一级标签为它自己的ID
songCountint直接打上该标签的歌曲数量,仅供参考;一、二级标签一般为 0
childrenarray下一级标签,结构相同;没有下级时为空数组
返回示例(每层只保留一个节点)
GET/open/v1/filters筛选项字典filters

无参数。返回筛选面板所需的取值范围和选项,建议客户端按它渲染,取值变化时无需改代码。

返回字段
字段类型说明
bpmobjectBPM 取值范围:min 最小值、max 最大值,两端都含。当前为 0–300
durationobject时长取值范围(秒),结构同 bpm。当前为 0–1800
bpmBucketsarrayBPM 推荐分档,仅作为界面建议,搜索时仍可传任意区间。每项:label 展示名称、begin 起点(含)、end 终点(不含)
durationBucketsarray时长推荐分档(秒),结构同 bpmBuckets
lyricTypesarray歌词类型选项。每项:value 取值(int)、label 展示名称
sortsarray排序方式选项。每项:value 取值(string,填入搜索的 sort)、label 展示名称
pageRulesarray各接口分页约束。每项:apiCode 接口编码、pageSize 每页条数、maxPage 最多可翻页数
tagCombineRulestring标签组合规则的文字说明,可直接展示给用户
返回示例
GET/open/v1/song/detail歌曲详情song.detail

查询单首歌曲的完整信息,包含波形、歌词等播放页所需的数据(不含音频地址,音频请用「试听歌曲」获取)。

请求参数(查询串)
参数类型说明
songIdstring,必填歌曲ID
data 包含搜索结果的全部字段,另外增加
字段类型说明
songDescstring歌曲描述
songVersionstring歌曲版本,如「完整版」「30秒版」
lyriciststring词作者
composerstring曲作者
arrangerstring编曲
recordLabelstring厂牌
albumNameEnstring专辑英文名
chorusBeginTimeint副歌开始时间(秒),可用于试听时直接跳到副歌
chorusEndTimeint副歌结束时间(秒)
oscillogramJsonUrlstring波形数据 JSON 地址(30 分钟有效),用于绘制波形图
lyricFileUrlstring歌词文件地址(30 分钟有效),格式见 lyricType
返回示例
GET/open/v1/playlist/list歌单列表playlist.list

分页浏览系统歌单或工作空间歌单。

请求参数(查询串)
参数类型说明
typeint1 系统歌单(默认),2 工作空间歌单
pageint页码,默认 1,最大 2
data 为分页结构,records 中每个歌单的字段
字段类型说明
playlistIdstring歌单ID
playlistTypeint1 系统歌单,2 工作空间歌单;查询歌单详情时原样传入 type
playlistNamestring歌单名称
subtitlestring副标题,仅工作空间歌单有
descriptionstring歌单描述
coverUrlstring封面图地址(30 分钟有效)
songCountint歌单内歌曲数量
tagNameListarray<string>歌单标签名称,仅系统歌单有;没有时为空数组
返回示例(type=2,只保留一条记录)
GET/open/v1/playlist/detail歌单详情playlist.detail

查询歌单信息和歌单内的歌曲,歌曲分页返回。

请求参数(查询串)
参数类型说明
playlistIdstring,必填歌单ID
typeint歌单类型,需与列表中的 playlistType 一致,默认 1
pageint歌曲页码,默认 1,最大 10
返回字段
字段类型说明
playlistobject歌单信息,字段同歌单列表
songsobject歌单内的歌曲,分页结构;records 中每首歌的字段同歌曲搜索

歌单内已下架的歌曲不会出现,也不计入 songs.total,所以它可能小于 playlist.songCount。

返回示例(只保留一首歌)
POST/open/v1/song/download下载歌曲song.download

获取歌曲的 MP3 下载地址,占用下载额度。请求体为 JSON。

请求参数(JSON 请求体)
参数类型说明
songIdstring,必填歌曲ID
userNamestring,必填终端用户名称,最长 100 字
songName / performerstring,可选传了会与曲库核对,不一致返回 40001,用于防止误下载
返回字段
字段类型说明
songIdstring歌曲ID
songNamestring歌曲名称
performerstring表演者
userNamestring终端用户名称,即请求中传入的值
downloadUrlstring音频下载地址(30 分钟有效),过期后用相同参数再请求一次即可,不会重复计量
formatstring音频格式,一般为 mp3
newDownloadboolean本次是否新产生了一次下载并占用额度;重复请求为 false
remainingQuotaint剩余下载总额度;未设置总额度时为 null
chargedboolean本次是否记了一次下载费用;同一首歌 10 分钟内重复下载为 false
chargeAmountnumber本次扣费金额(元);包年、测试模式或未计费时为 0
计量规则:同一账号下,同一首歌 + 同一终端用户只计一次下载。重复请求(包括网络重试、地址过期后重新获取)newDownload 为 false,不再占用额度。
计费规则:按次计费的账号,同一首歌 10 分钟内重复下载只扣一次费用(不区分终端用户),见下文「计费说明」。
返回示例
GET/open/v1/song/play试听歌曲song.play

获取歌曲的试听地址,用于在你的产品里在线播放。不占用下载额度,也不出具授权书;需要下载使用时请调用「下载歌曲」。

请求参数(查询串)
参数类型说明
songIdstring,必填歌曲ID
返回字段
字段类型说明
songIdstring歌曲ID
songNamestring歌曲名称
performerstring表演者
playUrlstring试听地址(完整音频,10 分钟有效),请只用于在线播放,不要缓存或转存
formatstring音频格式,一般为 mp3
expiresInint地址有效期(秒),目前为 600
chargedboolean本次是否记了一次试听费用;同一首歌 10 分钟内重复试听为 false
chargeAmountnumber本次扣费金额(元);包年、测试模式或未计费时为 0
试听地址过期后再调用一次即可拿到新地址;10 分钟内重复获取不重复计费。
返回示例

计费说明

计费方式按合同约定,二选一。你的计费方式、单价和余额可在控制台「费用」页查看。

计费方式说明价格
包年按合同约定的年费结算,有效期内调用不按次扣费以合同为准
预付 + 按次先预存费用,每次试听、下载从余额中扣除;每次充值不低于 5 万元试听 0.02 元/次,下载 2.30 元/次
规则说明
试听调用「试听歌曲」接口记一次试听费用
下载调用「下载歌曲」接口记一次下载费用,与是否新占用下载额度无关
10 分钟去重同一账号下同一首歌,从上次计费起 10 分钟内重复试听只记一次、重复下载只记一次,不区分终端用户;试听和下载分开计算
测试模式只记录计费明细,不扣余额
余额不足接口不会中断,余额可为负数,欠费部分在下次充值时抵扣;控制台会提示余额不足
只有「试听歌曲」和「下载歌曲」两个接口计费,检索、详情、歌单等接口不计费。接口返回的 charged 和 chargeAmount 表示本次请求是否计费、扣了多少钱。

额度与限流

限制说明超出时
每秒请求数按账号计,具体数值以合同为准,可在控制台「概览」查看42900
每日调用量按账号计所有接口,合同约定时生效42901
每日下载量按新产生的下载计,合同约定时生效42902
下载总额度按新产生的下载计,合同约定时生效42903

「每日」按北京时间自然日计算。收到 429xx 时请降低频率或次日再试,不要立即重试。

平台高峰期可能返回 50300(服务繁忙),这与账号额度无关,等待 1–2 秒后重试即可。建议同一账号的并发请求不超过 3 个。

错误码

codeHTTP说明处理建议
0200成功
40001400参数错误,msg 给出具体原因按提示修正参数
40100401AppKey 无效检查 X-App-Key
40101401签名错误用「校验用例」核对签名实现
40102401时间戳超出允许范围(±300 秒)同步服务器时钟
40103401请求重放(随机串重复)每次请求重新生成 X-Nonce
40104401缺少鉴权请求头补全 4 个请求头
40300403账号已停用或已过有效期联系我们
40301403接口未开通联系我们
40302403来源 IP 不在白名单内确认出口 IP 或联系我们调整
40400404歌曲不存在或已下架从列表中移除该歌曲
40401404歌单不存在或已下架刷新歌单列表
42900429请求过于频繁降低频率后重试
42901429已达今日调用上限次日再试
42902429已达今日下载上限次日再试
42903429下载额度已用完联系我们续约
50000500服务内部错误稍后重试;持续出现请提供 requestId
50300503服务繁忙,请稍后重试等待 1–2 秒后重试,并适当降低并发

常见问题

下载地址过期了怎么办?
用同样的参数再调一次「下载歌曲」即可拿到新地址,不会重复占用额度。
试听地址和下载地址有什么区别?
试听地址 10 分钟有效,按试听计费,不占用下载额度、不出具授权书,只能用于在线播放;需要在作品中使用音乐时,请调用「下载歌曲」。
怎么查看自己的余额和扣费明细?
在控制台「费用」页查看余额、每笔试听和下载的扣费明细和充值记录,并可导出对账单。
怎么查看自己的调用量和下载记录?
用我方为你绑定的手机号登录官网,进入开放平台控制台,可以查看用量统计、调用明细(支持按 requestId 查询)和下载记录,并导出下载对账单。
AppSecret 泄露了怎么办?
在控制台「凭证管理」里自助重置,旧密钥立即失效,新密钥只显示一次;也可以联系我们重置。
反馈问题需要提供什么?
请提供 requestId(或响应头 X-Request-Id)、请求时间和接口路径。
已经开通了?
用绑定的手机号登录官网,在控制台查看调用量、下载记录和费用。
进入控制台
成都声岛科技有限公司|蜀ICP备2025145483号-2|声岛科技(武汉)有限公司|Copyright@2025.All Rights Reserved|鄂ICP备2025142461号-1|鄂网文(2025)2459-577号