/meds/{medId}/file/upload 接口介绍
接口背景
/meds/{medId}/file/upload 提供给商户的 MED 证据文件上传接口。商户可为特定的 MED 上传分析文件(证据文件),如交易凭证、沟通记录等。每个文件必须通过 evidenceType 归类到该 MED 的证据材料清单,上传后将被安全存储并与该 MED 关联,用于后续分析。
仅当 MED 处于 WAITING 或 EVIDENCE_REQUIRED 状态且未过举证截止时间(dueTime)时才能上传文件;在 MED 保持上述状态期间,可为单个 MED 上传多个文件。
接口请求地址
| 项目 | 内容 |
|---|---|
| 请求方式 | POST |
| 请求路径 | /meds/{medId}/file/upload |
| Content-Type | multipart/form-data |
| 接口用途 | 为 MED 上传分析证据文件 |
认证
使用 ES256 请求签名,必须携带 X-Merchant-Id、X-Timestamp、X-Nonce、Digest 和 Authorization。X-Merchant-Id 填写商户号,keyId 为密钥版本号,默认 v1。签名值为 DER 编码的 ECDSA 签名经标准 Base64 编码后的结果,具体规则见 请求签名。
示例中的时间戳、Nonce、摘要和签名占位符需按每次实际请求生成;签名串包含实际路径及原始 query,分页或筛选条件变化后必须重新签名。
接口请求字段
| 字段名 | 位置 | 类型 | 是否必填 | 说明 |
|---|---|---|---|---|
medId | Path | string | 是 | MED 违规报告唯一标识符(平台案件号,medc 前缀 + 数字),最大长度 64 个字符。 |
evidenceType | Body | string | 是 | 证据材料类型,必须是该 MED 当前材料清单中的有效类型。 |
file | Body | file | 是 | 分析证据文件;支持 PDF、DOC、DOCX、TXT、JPG、JPEG、PNG 格式,最大 10 MB。出于安全考虑,文件扩展名会与内容类型(content type)及文件内容进行校验。 |
请求示例
Digest 针对实际发送的完整请求体字节计算,服务端收到后会重算比对。因此各环节必须使用同一份字节:先拼好完整请求体文件,再对它的全部字节计算摘要并签名,最后原样发送;任何环节改变字节都会导致验签失败。
第一步:拼出 .multipart 请求体文件。 boundary 固定为 MED_UPLOAD_BOUNDARY,不能随机生成(Content-Type 头中的 boundary 必须与文件内一致)。PDF 示例包含 evidenceType=MERCHANT_ORDER_RECORD 和 file(evidence-document.pdf,application/pdf);图片示例包含 evidenceType=LOGIN_IP_DEVICE_RECORD 和 file(screenshot-proof.png,image/png)。文件必须包含全部表单字段、分段头、文件原始字节及 CRLF 分隔符,结构如下(\r\n 表示回车加换行两个字节;文件内容原样嵌入,不是 Base64):
--MED_UPLOAD_BOUNDARY\r\n
Content-Disposition: form-data; name="evidenceType"\r\n
\r\n
MERCHANT_ORDER_RECORD\r\n
--MED_UPLOAD_BOUNDARY\r\n
Content-Disposition: form-data; name="file"; filename="evidence-document.pdf"\r\n
Content-Type: application/pdf\r\n
\r\n
<evidence-document.pdf 的原始字节>\r\n
--MED_UPLOAD_BOUNDARY--\r\n生成该文件并计算 Digest 的各语言示例(图片示例替换 evidenceType、文件名和 Content-Type 即可):
// Go 1.16+,仅标准库(更低版本可用 ioutil 包)。
package main
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"fmt"
"os"
)
const boundary = "MED_UPLOAD_BOUNDARY"
func main() {
fileBytes, err := os.ReadFile("evidence-document.pdf")
if err != nil {
panic(err)
}
var body bytes.Buffer
body.WriteString("--" + boundary + "\r\n")
body.WriteString("Content-Disposition: form-data; name=\"evidenceType\"\r\n\r\n")
body.WriteString("MERCHANT_ORDER_RECORD\r\n")
body.WriteString("--" + boundary + "\r\n")
body.WriteString("Content-Disposition: form-data; name=\"file\"; filename=\"evidence-document.pdf\"\r\n")
body.WriteString("Content-Type: application/pdf\r\n\r\n")
body.Write(fileBytes)
body.WriteString("\r\n--" + boundary + "--\r\n")
if err := os.WriteFile("evidence-document.multipart", body.Bytes(), 0o644); err != nil {
panic(err)
}
// 对完整请求体的全部字节计算 Digest,而不是只对 PDF 文件本身
sum := sha256.Sum256(body.Bytes())
fmt.Println("SHA-256=" + base64.StdEncoding.EncodeToString(sum[:]))
}// Java 8+,仅标准库。
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.util.Base64;
public class BuildMultipartBody {
public static void main(String[] args) throws Exception {
String boundary = "MED_UPLOAD_BOUNDARY";
byte[] fileBytes = Files.readAllBytes(Paths.get("evidence-document.pdf"));
StringBuilder head = new StringBuilder();
head.append("--").append(boundary).append("\r\n");
head.append("Content-Disposition: form-data; name=\"evidenceType\"\r\n\r\n");
head.append("MERCHANT_ORDER_RECORD\r\n");
head.append("--").append(boundary).append("\r\n");
head.append("Content-Disposition: form-data; name=\"file\"; filename=\"evidence-document.pdf\"\r\n");
head.append("Content-Type: application/pdf\r\n\r\n");
byte[] headBytes = head.toString().getBytes(StandardCharsets.UTF_8);
byte[] tailBytes = ("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8);
byte[] body = new byte[headBytes.length + fileBytes.length + tailBytes.length];
System.arraycopy(headBytes, 0, body, 0, headBytes.length);
System.arraycopy(fileBytes, 0, body, headBytes.length, fileBytes.length);
System.arraycopy(tailBytes, 0, body, headBytes.length + fileBytes.length, tailBytes.length);
Files.write(Paths.get("evidence-document.multipart"), body);
// 对完整请求体的全部字节计算 Digest,而不是只对 PDF 文件本身
String digest = "SHA-256=" + Base64.getEncoder().encodeToString(
MessageDigest.getInstance("SHA-256").digest(body));
System.out.println(digest);
}
}// Node.js 14+,仅内置模块。
import crypto from "node:crypto";
import fs from "node:fs";
const boundary = "MED_UPLOAD_BOUNDARY";
const fileBytes = fs.readFileSync("evidence-document.pdf");
const body = Buffer.concat([
Buffer.from(
`--${boundary}\r\n` +
'Content-Disposition: form-data; name="evidenceType"\r\n\r\n' +
"MERCHANT_ORDER_RECORD\r\n" +
`--${boundary}\r\n` +
'Content-Disposition: form-data; name="file"; filename="evidence-document.pdf"\r\n' +
"Content-Type: application/pdf\r\n\r\n"
),
fileBytes,
Buffer.from(`\r\n--${boundary}--\r\n`),
]);
fs.writeFileSync("evidence-document.multipart", body);
// 对完整请求体的全部字节计算 Digest,而不是只对 PDF 文件本身
const digest = "SHA-256=" + crypto.createHash("sha256").update(body).digest("base64");
console.log(digest);<?php
// PHP 7.1+,仅标准扩展。
$boundary = 'MED_UPLOAD_BOUNDARY';
$fileBytes = file_get_contents('evidence-document.pdf');
$body =
"--{$boundary}\r\n" .
'Content-Disposition: form-data; name="evidenceType"' . "\r\n\r\n" .
"MERCHANT_ORDER_RECORD\r\n" .
"--{$boundary}\r\n" .
'Content-Disposition: form-data; name="file"; filename="evidence-document.pdf"' . "\r\n" .
"Content-Type: application/pdf\r\n\r\n" .
$fileBytes .
"\r\n--{$boundary}--\r\n";
file_put_contents('evidence-document.multipart', $body);
// 对完整请求体的全部字节计算 Digest,而不是只对 PDF 文件本身
echo 'SHA-256=' . base64_encode(hash('sha256', $body, true)), PHP_EOL;# Python 3.7+,仅标准库。
import base64
import hashlib
from pathlib import Path
boundary = "MED_UPLOAD_BOUNDARY"
file_bytes = Path("evidence-document.pdf").read_bytes()
body = (
f"--{boundary}\r\n"
'Content-Disposition: form-data; name="evidenceType"\r\n\r\n'
"MERCHANT_ORDER_RECORD\r\n"
f"--{boundary}\r\n"
'Content-Disposition: form-data; name="file"; filename="evidence-document.pdf"\r\n'
"Content-Type: application/pdf\r\n\r\n"
).encode() + file_bytes + f"\r\n--{boundary}--\r\n".encode()
Path("evidence-document.multipart").write_bytes(body)
# 对完整请求体的全部字节计算 Digest,而不是只对 PDF 文件本身
digest = "SHA-256=" + base64.b64encode(hashlib.sha256(body).digest()).decode()第二步:生成签名。 使用上一步得到的 Digest,按请求签名构造签名串并加签((request-target) 行使用实际请求路径 /meds/{medId}/file/upload),得到 Authorization 头。
第三步:原样发送。 用 --data-binary 将 .multipart 文件一字节不改地发出,见下方示例(Python 中等价于把 body 直接作为请求数据传递,不要使用 files= 等会重新构造请求体的参数)。
两个常见错误都会导致验签失败:
- 只对上传文件本身计算摘要——服务端是对收到的完整 multipart 请求体重算的,两个摘要必然不一致。
- 签名后用
--form(或files=等自动构造 multipart 的参数)重新生成 boundary 或请求体——客户端会重新随机生成 boundary 并自行排列字段,发送的字节与签名时不同。
PDF 文件示例:
curl --request POST \
--url '<base_url>/meds/medc2874510938274639021/file/upload' \
--header 'X-Merchant-Id: <MERCHANT_ID>' \
--header 'X-Timestamp: <UNIX_TIMESTAMP_SECONDS>' \
--header 'X-Nonce: <UNIQUE_NONCE>' \
--header 'Digest: SHA-256=<MULTIPART_BODY_SHA256_BASE64>' \
--header 'Authorization: Signature keyId="v1",alg="ES256",headers="(request-target) x-timestamp x-nonce digest",signature="<ES256_SIGNATURE_BASE64>"' \
--header 'Content-Type: multipart/form-data; boundary=MED_UPLOAD_BOUNDARY' \
--data-binary '@evidence-document.multipart'图片文件示例:
curl --request POST \
--url '<base_url>/meds/medc2874510938274639021/file/upload' \
--header 'X-Merchant-Id: <MERCHANT_ID>' \
--header 'X-Timestamp: <UNIX_TIMESTAMP_SECONDS>' \
--header 'X-Nonce: <UNIQUE_NONCE>' \
--header 'Digest: SHA-256=<MULTIPART_BODY_SHA256_BASE64>' \
--header 'Authorization: Signature keyId="v1",alg="ES256",headers="(request-target) x-timestamp x-nonce digest",signature="<ES256_SIGNATURE_BASE64>"' \
--header 'Content-Type: multipart/form-data; boundary=MED_UPLOAD_BOUNDARY' \
--data-binary '@screenshot-proof.multipart'接口响应字段
接口使用统一的 status、msg、data 响应结构。已上传文件的信息位于 data 中。
| 字段名 | 类型 | 是否必返 | 说明 |
|---|---|---|---|
status | int | 是 | 响应码 |
msg | string | 是 | 与 status 对应 |
data | object | 成功必返 | 已上传文件的信息;请求在验签或协议解析阶段失败时可能不返回。 |
data.id | string | 是 | 上传文件的唯一标识符。 |
data.medId | string | 是 | 该文件关联的 MED ID。 |
data.subMerchantNo | string | 是 | 该 MED 所属二级商户号;直连商户固定为空字符串,可忽略。 |
data.evidenceType | string | 是 | 该文件归属的证据材料类型。 |
data.createdAt | string | 是 | 文件上传时间,ISO 8601 UTC 时间字符串(yyyy-MM-ddTHH:mm:ssZ)。 |
响应示例
{
"status": 200,
"msg": "sucesso",
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"medId": "medc2874510938274639021",
"subMerchantNo": "",
"evidenceType": "MERCHANT_ORDER_RECORD",
"createdAt": "2026-06-02T09:10:00Z"
}
}响应错误码
业务规则
文件上传要求
- 最大文件大小为 10 MB。
- 支持格式:PDF、DOC、DOCX、TXT、JPG、JPEG、PNG。
- 出于安全考虑,文件扩展名会与内容类型(content type)进行校验。
- 每次上传只能指定一个
evidenceType,同一材料类型可以上传多个文件。 evidenceType必须来自查询证据材料要求接口返回的data.requirements[].evidenceType。- 操作方必须有权访问与该 MED 关联的账户。
MED 状态校验
- 仅当 MED 处于
WAITING或EVIDENCE_REQUIRED状态时才能上传文件。 - MED 处于其他状态时,不能上传文件;平台打回(
EVIDENCE_REQUIRED)后可补充上传。 - 已提交分析且待平台审核(
UNDER_REVIEW待审核)期间,不能再上传文件。 - 已过举证截止时间(
dueTime)后,不能上传文件。 - 在 MED 保持
WAITING或EVIDENCE_REQUIRED状态期间,可为单个 MED 上传多个文件。
返回 MED API 总览