认证测试
所有对 QI DTVM API 的请求都携带两个 header:
API-CLIENT-KEY:您集成的 API key。AUTHORIZATION:使用您的私钥签名的 JWT。QI 使用您在密钥交换中注册的公钥验证签名。
本页面用 Python 构造这两个 header 并调用 /authentication_test;只有当主机、API key 和签名都正确时,该端点才返回 200。请在进行任何其他调用之前先完成此测试。
1. 选择您所属角色的 Base URL
这是首次调用中最常见的错误:复制了使用其他角色主机的示例。
每种角色都有自己的主机。API key 只在其签发角色对应的主机上有效:管理人的 API key 发送到转让方的主机会被拒绝并返回 Invalid API Key,即使签名正确也是如此。
| 您的角色 | Sandbox | 生产环境 |
|---|---|---|
| 管理人(基金管理机构) | https://manager-api.sandbox.qidtvm.com.br | https://manager-api.qidtvm.com.br |
| 顾问(顾问机构) | https://consultant-api.sandbox.qidtvm.com.br | https://consultant-api.qidtvm.com.br |
| 转让方 | https://assignor-api.sandbox.qidtvm.com.br | https://assignor-api.qidtvm.com.br |
| 发起人 | https://originator-api.sandbox.qidtvm.com.br | 由集成团队提供 |
| 投资者 | https://investor-api.sandbox.qidtvm.com.br | https://investor-api.qidtvm.com.br |
| 分销商 | https://distributor-api.sandbox.qidtvm.com.br | https://distributor-api.qidtvm.com.br |
请使用创建集成时 QI 告知的角色;如果您是通过门户创建的集成,则使用注册该集成的门户所对应的角色。Sandbox 的 API key 在生产环境中无效,反之亦然。
请直接从门户复制 Base URL:它显示在集成的 Credenciais 区块中、API Key 旁边,已对应正确的角色和环境。参见第 3 步中的图片。
2. 安装并导入依赖库
pip install "python-jose[cryptography]" requests
from datetime import datetime, timezone
import json
from hashlib import md5
from jose import jwt
import requests
3. 填写 API key、私钥和算法
api_key = "YOUR_API_KEY"
client_private_key = """-----BEGIN PRIVATE KEY-----
YOUR_PRIVATE_KEY
-----END PRIVATE KEY-----"""
algorithm = "RS256" # 您密钥的算法;见下表
各个值的获取位置取决于您的集成是如何创建的:
| 值 | 管理人和顾问(门户) | 转让方、发起人、投资者和分销商 |
|---|---|---|
api_key | 在门户的 Gestão de Acesso > Integração API 中打开集成,复制 Credenciais 区块中的 API Key 字段。 | 完成密钥交换后由 QI 集成团队发送。 |
client_private_key | 在门户中生成密钥对时下载的 qi-integration-...-private-key.pem 文件,或您自行生成的密钥对中的私钥。 | 您在密钥交换中生成的密钥对中的私钥。 |
algorithm | 门户中所选算法下方提示的值:RSA 使用 RS256(门户默认),EC P-256、P-384 或 P-521 分别使用 ES256、ES384 或 ES512。 | ES512,对应密钥交换中生成的 EC P-521 密钥。 |

粘贴私钥文件的全部内容,包括 BEGIN 和 END 行——根据生成密钥对的工具不同,头部可能是 PRIVATE KEY 或 EC PRIVATE KEY。API key 标识集成,而私钥签名证明请求来自您:请将两者都视为凭证。私钥绝不会发送给 QI。在生产环境中,请从密钥保管库或环境变量中加载私钥——切勿将其写在源代码中。
门户中从创建集成到注册公钥的完整步骤,参见通过门户集成。
4. 定义请求数据
# 替换为您所属角色及环境的 URL(见第 1 步的表格)。
base_url = "YOUR_PROFILE_BASE_URL" # 例如: "https://manager-api.sandbox.qidtvm.com.br"
method = "POST"
endpoint = "/authentication_test"
body = {"name": "QI Tech"}
签名中的 uri 字段必须与请求中发送的路径完全一致。如果端点在 query string 中接收参数,这些参数也属于签名的 URI——参见带 query string 的请求。
5. 构造签名内容
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
dict_to_sign = {"timestamp": timestamp, "method": method, "uri": endpoint}
timestamp 采用 AAAA-MM-DDTHH:MM:SS 格式,不带时区和毫秒,始终为 UTC,与 QI 服务器时间的偏差在 10 分钟以内才会被接受。每次请求都需生成新的 timestamp,并保持服务器时钟同步。
5.1. 如有 body,加入 MD5
带 body 的请求需包含将要发送的确切字节的 MD5:
body_bytes = json.dumps(body).encode()
dict_to_sign["payload_md5"] = md5(body_bytes).hexdigest()
在第 7 步中必须原样发送这些 body_bytes。如果 JSON 被重新序列化——字段顺序、空格或重音字符编码发生 变化——MD5 就会不一致。
6. 签名 JWT
encoded_header_token = jwt.encode(
claims=dict_to_sign,
key=client_private_key,
algorithm=algorithm,
headers={"alg": algorithm, "typ": "JWT"},
)
algorithm 必须与密钥类型匹配:RSA 使用 RS*,EC 使用 ES*。否则 jwt.encode 会在请求发出前失败,并抛出描述不清晰的 JWSError——请先检查 algorithm。
7. 发送请求
headers = {
"API-CLIENT-KEY": api_key,
"AUTHORIZATION": encoded_header_token,
"Content-Type": "application/json",
}
resp = requests.post(url=f"{base_url}{endpoint}", headers=headers, data=body_bytes)
print(resp.status_code, resp.json())
预期响应为 200,内容为 {"name": "QI Tech", "success": "Congrats!"}。GET 和 POST /authentication_test 的接口契约参见测试端点。
认证错误
所有主机上认证错误的 code 结尾数字都相同。前缀表示响应的主机:MIT 表示 manager-api,CIT 表示 consultant-api,AIT 表示 assignor-api。如果前缀不属于您的角色,说明 base_url 有误。
code 结尾 | 状态码 | 错误 | 检查项 |
|---|---|---|---|
000007 | 401 | 缺少 header | 是否同时发送了 API-CLIENT-KEY 和 AUTHORIZATION 两个 header。 |
000008 | 401 | API key 无效 | base_url 是否为您所属角色及正确环境的 URL,API key 复制时是否带有空格。 |
000009 | 400 | 集成未启用 | 集成是否处于启用状态。请联系 integracao.dtvm@qitech.com.br。 |
000010 | 401 | 公钥待注册 | 集成是否已注册公钥。 |
000011 | 401 | 认证无效 | JWT 是否使用与已注册公钥对应的私钥签名。 |
000012 | 401 | 签名不完整 | JWT 是否包含 timestamp、method 和 uri 字段。 |
000013 | 401 | 方法无效 | 签名中的 method 是否与请求方法一致,且为大写。 |
000014 | 401 | 签名已过期 | timestamp 是否为 UTC、是否在请求时生成,以及服务器时钟是否同步。 |
000015 | 401 | 端点无效 | 签名中的 uri 是否与发送的路径完全一致,包含 query string,且不含 base_url。 |
000016 | 401 | MD5 无效 | payload_md5 是否基于 body 中发送的相同字节计算。 |
000020 | 401 | timestamp 格式错误 | timestamp 是否为 AAAA-MM-DDTHH:MM:SS 格式,不带时区和毫秒。只有 manager-api 和 consultant-api 返回此代码:在 assignor-api 上,格式错误的 timestamp 会返回 500 错误。 |
权限、路由和各服务的错误参见 API 错误。
带 query string 的请求
分页或带筛选条件的端点通过 query string 接收参数——例如 ?page=0&limit=20。这种情况下,query string 属于签名的 URI。
uri 字段必须与请求中发送的路径一致:参数相同、顺序相同、编码相同。如果签名的 URI 与发送的 URI 不一致,请求会被拒绝并返回状态码 401:
{
"title": "Invalid endpoint",
"description": "Invalid endpoint.",
"translation": "O endpoint é invalido",
"code": "MIT000015"
}
示例
# base_url 沿用第 4 步。此端点属于管理人(manager-api)。
path = "/quota/fund_class/{fund_class_key}/investor_positions"
query_string = "page=0&limit=20"
# query string 属于签名的 uri
uri = f"{path}?{query_string}"
method = "GET"
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
dict_to_sign = {"timestamp": timestamp, "method": method, "uri": uri}
encoded_header_token = jwt.encode(
claims=dict_to_sign,
key=client_private_key,
algorithm=algorithm,
headers={"alg": algorithm, "typ": "JWT"},
)
headers = {"API-CLIENT-KEY": api_key, "AUTHORIZATION": encoded_header_token}
resp = requests.get(url=f"{base_url}{uri}", headers=headers)
print(resp.json())
建议
- 先构造好 query string,再发送完整 URL。避免在已签名的 URI 上同时使用
requests的params参数:HTTP 客户端可能会重新排序或重新编码参数,导致签名失效。 GET请求通常没有 body。这种情况下,请勿发送 body,也不要包含payload_md5字段。- 包含特殊字符的值(证件号中的标点、日期、空格)必须以最终发送到 URL 中的格式进行签名。