跳转至

查询日账单/用量明细

本文说明如何通过 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_periodpage_num),不要使用 groupPeriod 等 camelCase。租户由 AK/SK 自动识别,无需tenant_id

日账单场景下,不必再传 start_time / end_timeperiodYYYY-MM)已限定账月,group_period=GROUP_PERIOD_DAILY 会把该月内明细按自然日汇总。仅当需要查询账月中某几天时,才额外传入 start_timeend_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 示例

GET https://openapi.magikcloud.cn/api/v1/usages/details?date=2026-05-27&page_num=1&page_size=20

响应示例

{
  "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-DigestSignature-InputSignature

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 → ScriptsPre-request → 粘贴 postman_pre_request_script.js 全文
  • 单条请求:该请求 → ScriptsPre-request → 同上

发送前脚本会写入三个 Header,无需手写。

3. 配置请求

  • MethodGET
  • 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