查询日账单/用量明细
本文说明如何通过 MagikCloud Open API,按自然日汇总查询账单/用量明细。
前置条件
请先完成 接入流程 中的 AK/SK 获取;请求须按 构建请求 完成 AK/SK 签名。
查询账单明细
接口说明
| 项 | 值 |
|---|---|
| Base URL | https://openapi.magikcloud.cn |
| 路径 | /api/v1/bills/details |
| 方法 | GET |
查询参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
period |
string | 是 | 账期,格式 YYYY-MM(如 2026-05),与控制台账单页一致 |
group_period |
enum | 是(日账单) | 固定为 GROUP_PERIOD_DAILY,服务端按自然日聚合 |
start_time |
string | 否 | 可选:在 period 账期内再缩小起始日,RFC 3339 |
end_time |
string | 否 | 可选:在 period 账期内再缩小结束日,RFC 3339 |
page_num |
int | 否 | 页码,默认 1 |
page_size |
int | 否 | 每页条数,默认由服务端处理 |
project_id |
string | 否 | 按项目筛选 |
group_term |
enum | 否 | 聚合维度,如 GROUP_TERM_INSTANCE(按实例)、GROUP_TERM_ITEM(按计费项);未传时服务端默认 GROUP_TERM_ITEM |
product |
string | 否 | 产品类型,如 PRODUCT_INFERENCE_ENDPOINT |
instance_id |
string | 否 | 按实例 ID 筛选 |
bill_id / order_id |
string | 否 | 按账单或订单筛选 |
Query 参数请使用 snake_case(如 group_period、page_num),不要使用 groupPeriod 等 camelCase。租户由 AK/SK 自动识别,无需传 tenant_id。
日账单场景下,不必再传 start_time / end_time:period(YYYY-MM)已限定账月,group_period=GROUP_PERIOD_DAILY 会把该月内明细按自然日汇总。仅当需要查询账月中某几天时,才额外传入 start_time、end_time 做范围过滤。
请求 URL 示例
查询 2026 年 5 月整月、按日汇总的账单:
GET https://openapi.magikcloud.cn/api/v1/bills/details?period=2026-05&group_period=GROUP_PERIOD_DAILY&page_num=1&page_size=20
仅查询账期内部分日期时(可选):
GET https://openapi.magikcloud.cn/api/v1/bills/details?period=2026-05&group_period=GROUP_PERIOD_DAILY&start_time=2026-05-27T00:00:00Z&end_time=2026-05-31T23:59:59Z&page_num=1&page_size=20
响应示例
{
"code": 0,
"reason": "OK",
"message": "success",
"data": {
"list": [
{
"tenantId": "tn-xxx",
"projectId": "prj-xxx",
"period": "2026-05",
"instanceId": "ep-xxx",
"product": "PRODUCT_INFERENCE_ENDPOINT",
"startTime": "2026-05-27T00:00:00Z",
"endTime": "2026-05-27T23:59:59Z",
"payableAmount": "12.34",
"usageValue": "1000000",
"item": "ITEM_PROMPT_TOKEN"
}
],
"total": 1
}
}
list 中每条记录代表一个自然日(在 group_period=GROUP_PERIOD_DAILY 时)下的聚合结果;startTime / endTime 为该日的起止时间。金额、用量等字段均为字符串,与控制台账单明细一致。
Go 示例
package main
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
const (
baseURL = "https://openapi.magikcloud.cn"
path = "/api/v1/bills/details"
)
func main() {
accessKeyID := os.Getenv("ACCESS_KEY_ID")
secretKey := []byte(os.Getenv("SECRET_ACCESS_KEY"))
if accessKeyID == "" || len(secretKey) == 0 {
panic("请设置 ACCESS_KEY_ID 和 SECRET_ACCESS_KEY")
}
rawQuery := "period=2026-05" +
"&group_period=GROUP_PERIOD_DAILY" +
"&page_num=1" +
"&page_size=20"
req, err := http.NewRequest(http.MethodGet, baseURL+path+"?"+rawQuery, nil)
if err != nil {
panic(err)
}
if err := signRequest(req, accessKeyID, secretKey); err != nil {
panic(err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(resp.Status)
fmt.Println(string(body))
}
func signRequest(req *http.Request, accessKeyID string, secretKey []byte) error {
if req.Body != nil {
bodyBytes, err := io.ReadAll(req.Body)
if err != nil {
return err
}
_ = req.Body.Close()
sum := sha256.Sum256(bodyBytes)
digest := base64.URLEncoding.EncodeToString(sum[:])
req.Header.Set("Content-Digest", "sha-256="+digest)
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
} else {
sum := sha256.Sum256(nil)
req.Header.Set("Content-Digest", "sha-256="+base64.URLEncoding.EncodeToString(sum[:]))
}
nonceBytes := make([]byte, 32)
if _, err := rand.Read(nonceBytes); err != nil {
return err
}
nonce := base64.RawURLEncoding.EncodeToString(nonceBytes)
created := time.Now().Unix()
query := "?"
if req.URL.RawQuery != "" {
query += req.URL.RawQuery
}
cd := req.Header.Get("Content-Digest")
reqPath := req.URL.Path
if reqPath == "" {
reqPath = "/"
}
escape := func(s string) string {
s = strings.ReplaceAll(s, `\`, `\\`)
s = strings.ReplaceAll(s, `"`, `\"`)
return s
}
sigParams := `("@method" "@query" "@path" "content-digest")` + ";" + strings.Join([]string{
fmt.Sprintf("created=%d", created),
fmt.Sprintf(`nonce="%s"`, escape(nonce)),
fmt.Sprintf(`keyid="%s"`, escape(accessKeyID)),
`alg="hmac-sha256"`,
}, ";")
signatureBase := strings.Join([]string{
fmt.Sprintf(`"@method": %s`, req.Method),
fmt.Sprintf(`"@query": %s`, query),
fmt.Sprintf(`"@path": %s`, reqPath),
fmt.Sprintf(`"content-digest": %s`, cd),
fmt.Sprintf(`"@signature-params": %s`, sigParams),
}, "\n")
mac := hmac.New(sha256.New, secretKey)
mac.Write([]byte(signatureBase))
sig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
req.Header.Set("Signature-Input", "magik="+sigParams)
req.Header.Set("Signature", "magik=:"+sig+":")
return nil
}
查询用量明细
接口说明
| 项 | 值 |
|---|---|
| Base URL | https://openapi.magikcloud.cn |
| 路径 | /api/v1/usages/details |
| 方法 | GET |
查询参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
date |
string | 是 | 查询日期,格式 YYYY-MM-DD(如 2026-06-15)。按上海时区自然日统计,不能是未来日期。 |
page_num |
int | 否 | 页码,从 1 开始。默认 1。 |
page_size |
int | 否 | 每页条数。默认 100,最大 500。 |
project_id |
string | 否 | 按项目 ID 过滤。 |
model |
string | 否 | 按模型名过滤。 |
请求 URL 示例
响应示例
{
"code": 0,
"reason": "OK",
"message": "success",
"data": {
"list": [
{
"requestId": "req-xxx",
"requestTime": "2026-05-27T00:00:00Z",
"tenantId": "tn-xxx",
"model": "example-model",
"endpoint": "ep-xxx",
"apikey": "magik-***",
"promptTokens": 1000,
"completionTokens": 100,
"promptCachedTokens": 0
}
],
"total": "1",
"date": "2026-05-27"
}
}
list 中每条记录为一次推理请求的用量明细;total 为符合条件的总条数(字符串);date 为查询的自然日。Token 相关字段为整数,与控制台用量明细一致。
以下示例按 date 分页拉取当日全部用量明细:page_size 取上限 500,当本页 list 为空或长度小于 page_size 时结束。
Go 示例(全量拉取)
package main
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
const (
baseURL = "https://openapi.magikcloud.cn"
path = "/api/v1/usages/details"
pageSize = 500
)
type usageDetail struct {
RequestID string `json:"requestId"`
RequestTime string `json:"requestTime"`
TenantID string `json:"tenantId"`
Model string `json:"model"`
Endpoint string `json:"endpoint"`
APIKey string `json:"apikey"`
PromptTokens int `json:"promptTokens"`
CompletionTokens int `json:"completionTokens"`
}
type usageListResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
List []usageDetail `json:"list"`
} `json:"data"`
}
func main() {
accessKeyID := os.Getenv("ACCESS_KEY_ID")
secretKey := []byte(os.Getenv("SECRET_ACCESS_KEY"))
if accessKeyID == "" || len(secretKey) == 0 {
panic("请设置 ACCESS_KEY_ID 和 SECRET_ACCESS_KEY")
}
date := "2026-05-27"
var all []usageDetail
for pageNum := 1; ; pageNum++ {
list, err := fetchPage(date, pageNum, accessKeyID, secretKey)
if err != nil {
panic(err)
}
if len(list) == 0 {
break
}
all = append(all, list...)
if len(list) < pageSize {
break
}
}
out, err := json.MarshalIndent(all, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
func fetchPage(date string, pageNum int, accessKeyID string, secretKey []byte) ([]usageDetail, error) {
rawQuery := fmt.Sprintf("date=%s&page_num=%d&page_size=%d", date, pageNum, pageSize)
req, err := http.NewRequest(http.MethodGet, baseURL+path+"?"+rawQuery, nil)
if err != nil {
return nil, err
}
if err := signRequest(req, accessKeyID, secretKey); err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var parsed usageListResponse
if err := json.Unmarshal(body, &parsed); err != nil {
return nil, err
}
if parsed.Code != 0 {
return nil, fmt.Errorf("api error: code=%d message=%s", parsed.Code, parsed.Message)
}
return parsed.Data.List, nil
}
func signRequest(req *http.Request, accessKeyID string, secretKey []byte) error {
if req.Body != nil {
bodyBytes, err := io.ReadAll(req.Body)
if err != nil {
return err
}
_ = req.Body.Close()
sum := sha256.Sum256(bodyBytes)
digest := base64.URLEncoding.EncodeToString(sum[:])
req.Header.Set("Content-Digest", "sha-256="+digest)
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
} else {
sum := sha256.Sum256(nil)
req.Header.Set("Content-Digest", "sha-256="+base64.URLEncoding.EncodeToString(sum[:]))
}
nonceBytes := make([]byte, 32)
if _, err := rand.Read(nonceBytes); err != nil {
return err
}
nonce := base64.RawURLEncoding.EncodeToString(nonceBytes)
created := time.Now().Unix()
query := "?"
if req.URL.RawQuery != "" {
query += req.URL.RawQuery
}
cd := req.Header.Get("Content-Digest")
reqPath := req.URL.Path
if reqPath == "" {
reqPath = "/"
}
escape := func(s string) string {
s = strings.ReplaceAll(s, `\`, `\\`)
s = strings.ReplaceAll(s, `"`, `\"`)
return s
}
sigParams := `("@method" "@query" "@path" "content-digest")` + ";" + strings.Join([]string{
fmt.Sprintf("created=%d", created),
fmt.Sprintf(`nonce="%s"`, escape(nonce)),
fmt.Sprintf(`keyid="%s"`, escape(accessKeyID)),
`alg="hmac-sha256"`,
}, ";")
signatureBase := strings.Join([]string{
fmt.Sprintf(`"@method": %s`, req.Method),
fmt.Sprintf(`"@query": %s`, query),
fmt.Sprintf(`"@path": %s`, reqPath),
fmt.Sprintf(`"content-digest": %s`, cd),
fmt.Sprintf(`"@signature-params": %s`, sigParams),
}, "\n")
mac := hmac.New(sha256.New, secretKey)
mac.Write([]byte(signatureBase))
sig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
req.Header.Set("Signature-Input", "magik="+sigParams)
req.Header.Set("Signature", "magik=:"+sig+":")
return nil
}
Postman 调试
使用 postman_pre_request_script.js 脚本在每次发送前自动计算 Content-Digest、Signature-Input、Signature。
const ACCESS_KEY_ID = pm.environment.get("access_key_id") || pm.variables.get("access_key_id");
const SECRET_ACCESS_KEY = pm.environment.get("secret_access_key") || pm.variables.get("secret_access_key");
const EXPIRES_IN_SECONDS = parseInt(pm.environment.get("expires_in_seconds") || "300", 10);
const ALGORITHM = "hmac-sha256";
const SIGNATURE_LABEL = "magik";
const COMPONENTS = [
"@method",
"@query",
"@path",
"content-digest",
];
function escapeString(s) {
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}
function serializeComponentIdentifier(name) {
return `"${name}"`;
}
function serializeSignatureParams(components, params) {
let result = "(" + components.map(serializeComponentIdentifier).join(" ") + ")";
const pairs = [];
if (params.created) pairs.push(`created=${params.created}`);
if (params.expires) pairs.push(`expires=${params.expires}`);
if (params.nonce) pairs.push(`nonce="${escapeString(params.nonce)}"`);
if (params.keyid) pairs.push(`keyid="${escapeString(params.keyid)}"`);
if (params.alg) pairs.push(`alg="${escapeString(params.alg)}"`);
if (pairs.length > 0) result += ";" + pairs.join(";");
return result;
}
function base64UrlEncodeWordArray(wordArray) {
let b64 = CryptoJS.enc.Base64.stringify(wordArray);
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function base64UrlEncodeWordArrayWithPadding(wordArray) {
let b64 = CryptoJS.enc.Base64.stringify(wordArray);
return b64.replace(/\+/g, "-").replace(/\//g, "_");
}
function sha256Bytes(str) {
return CryptoJS.SHA256(str || "");
}
function hmacSha256Bytes(keyStr, dataStr) {
return CryptoJS.HmacSHA256(dataStr || "", keyStr || "");
}
function randomNonceBytes(len) {
const bytes = [];
for (let i = 0; i < len; i++) {
bytes.push(Math.floor(Math.random() * 256));
}
return CryptoJS.lib.WordArray.create(bytes);
}
function extractMessageComponents(req) {
let urlStr = pm.variables.replaceIn(req.url.toString());
if (!/^https?:\/\//i.test(urlStr)) {
urlStr = "https://" + urlStr.replace(/^\/+/, "");
}
let url;
try {
url = new URL(urlStr);
} catch (e) {
throw new Error(`Invalid URL: ${urlStr}, ${e.message}`);
}
const headerFields = {};
if (req.headers && req.headers.each) {
req.headers.each(h => {
const k = h.key.toLowerCase();
if (!headerFields[k]) headerFields[k] = [];
headerFields[k].push(h.value);
});
}
return {
method: (req.method || "GET").toUpperCase(),
scheme: url.protocol.replace(":", "").toLowerCase(),
authority: url.host.toLowerCase(),
path: url.pathname || "/",
query: url.search || "",
targetUri: url.toString(),
headerFields,
};
}
function getComponentValue(name, msg) {
switch (name) {
case "@method": return msg.method;
case "@query": {
const q = msg.query || "";
return q ? (q.startsWith("?") ? q : "?" + q) : "";
}
case "@path": return msg.path || "/";
default: {
const lower = name.toLowerCase();
const values = msg.headerFields[lower];
if (!values || !values.length) throw new Error(`Header field ${name} not found`);
return values.join(", ");
}
}
}
function buildSignatureBase(components, params, msg) {
let base = "";
for (const c of components) {
const id = serializeComponentIdentifier(c);
const val = getComponentValue(c, msg);
base += `${id}: ${val}\n`;
}
base += `"@signature-params": ${serializeSignatureParams(components, params)}\n`;
return base.replace(/\n$/, "");
}
function ensureContentDigestHeaderSync() {
const existing = pm.request.headers.get("content-digest");
if (existing) return;
let bodyString = "";
if (pm.request.body) {
try {
bodyString = pm.request.body.toString() || "";
} catch (_) {
bodyString = "";
}
}
const hashWordArray = sha256Bytes(bodyString);
const digest = base64UrlEncodeWordArrayWithPadding(hashWordArray);
const headerValue = `sha-256=${digest}`;
pm.request.headers.upsert({ key: "Content-Digest", value: headerValue });
}
try {
if (!ACCESS_KEY_ID || !SECRET_ACCESS_KEY) {
throw new Error("access_key_id / secret_access_key 未配置");
}
ensureContentDigestHeaderSync();
const msg = extractMessageComponents(pm.request);
const now = Math.floor(Date.now() / 1000);
const nonceBytes = randomNonceBytes(32);
const nonce = base64UrlEncodeWordArray(nonceBytes);
const sigParams = {
created: now,
expires: now + EXPIRES_IN_SECONDS,
nonce,
keyid: ACCESS_KEY_ID,
alg: ALGORITHM,
};
const sigBase = buildSignatureBase(COMPONENTS, sigParams, msg);
console.log("Signature Base:", sigBase);
const sigWordArray = hmacSha256Bytes(SECRET_ACCESS_KEY, sigBase);
const sigVal = base64UrlEncodeWordArray(sigWordArray);
const sigInput = `${SIGNATURE_LABEL}=${serializeSignatureParams(COMPONENTS, sigParams)}`;
const sigHeader = `${SIGNATURE_LABEL}=:${sigVal}:`;
pm.request.headers.remove("Signature-Input");
pm.request.headers.remove("Signature");
pm.request.headers.add({ key: "Signature-Input", value: sigInput });
pm.request.headers.add({ key: "Signature", value: sigHeader });
console.log("Signature-Input:", sigInput);
console.log("Signature:", sigHeader);
} catch (e) {
console.error("Signing error:", e);
throw e;
}
1. 环境变量(Environment)
在 Postman 里新建 Environment,至少配置:
| 变量 | 示例 | 说明 |
|---|---|---|
access_key_id |
AK... |
控制台 Access Key ID |
secret_access_key |
SK... |
Secret(勿提交到 Git) |
可选:expires_in_seconds(默认 300)。
2. 挂 Pre-request Script
任选其一:
- 整个 Collection:Collection → Scripts → Pre-request → 粘贴
postman_pre_request_script.js全文 - 单条请求:该请求 → Scripts → Pre-request → 同上
发送前脚本会写入三个 Header,无需手写。
3. 配置请求
- Method:
GET - URL(带 query),例如:
https://openapi.magikcloud.cn/api/v1/bills/details?period=2026-05&group_period=GROUP_PERIOD_DAILY&page_num=1&page_size=20
- Body:无(GET)
- 右上角选中上一步创建的 Environment