Teste de autenticação
Toda requisição às APIs da QI DTVM leva dois headers:
API-CLIENT-KEY: a chave de API da sua integração.AUTHORIZATION: um JWT assinado com a sua chave privada. A QI valida a assinatura com a chave pública que você cadastrou na troca de chaves.
Esta página monta os dois headers em Python e chama /authentication_test, que só responde 200 quando host, chave de API e assinatura estão corretos. Faça este teste antes de qualquer outra chamada.
1. Escolha a URL base do seu perfil
Este é o erro mais comum na primeira chamada: copiar o exemplo com o host de outro perfil.
Cada perfil tem o próprio host. A chave de API só é reconhecida no host do perfil para o qual foi emitida: a chave de uma gestora enviada para o host de cedentes é recusada como Invalid API Key, mesmo com a assinatura correta.
| Seu perfil | Sandbox | Produção |
|---|---|---|
| Gestor (gestora de fundos) | https://manager-api.sandbox.qidtvm.com.br | https://manager-api.qidtvm.com.br |
| Consultor (consultoria) | https://consultant-api.sandbox.qidtvm.com.br | https://consultant-api.qidtvm.com.br |
| Cedente | https://assignor-api.sandbox.qidtvm.com.br | https://assignor-api.qidtvm.com.br |
| Originador | https://originator-api.sandbox.qidtvm.com.br | Informado pelo time de integração |
| Investidor | https://investor-api.sandbox.qidtvm.com.br | https://investor-api.qidtvm.com.br |
| Distribuidor | https://distributor-api.sandbox.qidtvm.com.br | https://distributor-api.qidtvm.com.br |
Use o perfil informado pela QI quando a integração foi criada — ou, se você criou a integração pelo portal, o perfil do portal em que ela foi cadastrada. A chave de API de sandbox não funciona em produção, e vice-versa.
Copie a URL base direto do portal: ela aparece no bloco Credenciais da integração, ao lado da API Key, já no perfil e no ambiente certos. Veja a imagem no passo 3.
2. Instale e importe as bibliotecas
pip install "python-jose[cryptography]" requests
from datetime import datetime, timezone
import json
from hashlib import md5
from jose import jwt
import requests
3. Informe a chave de API, a chave privada e o algoritmo
api_key = "SUA_API_KEY"
client_private_key = """-----BEGIN PRIVATE KEY-----
SUA_CHAVE_PRIVADA
-----END PRIVATE KEY-----"""
algorithm = "RS256" # o algoritmo da sua chave; veja a tabela abaixo
Onde encontrar cada valor depende de como a sua integração foi criada:
| Valor | Gestoras e consultorias (portal) | Cedentes, originadores, investidores e distribuidores |
|---|---|---|
api_key | No portal, em Gestão de Acesso > Integração API: abra a integração e copie o campo API Key do bloco Credenciais. | Enviada pelo time de integração da QI depois da troca de chaves. |
client_private_key | O arquivo qi-integration-...-private-key.pem baixado ao gerar o par no portal, ou a chave privada do par que você mesmo gerou. | A chave privada do par que você gerou na troca de chaves. |
algorithm | O indicado no portal abaixo do algoritmo escolhido: RS256 para RSA (padrão do portal), ES256, ES384 ou ES512 para EC P-256, P-384 ou P-521. | ES512, para a chave EC P-521 gerada na troca de chaves. |

Cole o conteúdo inteiro do arquivo da chave privada, com as linhas BEGIN e END — o cabeçalho pode ser PRIVATE KEY ou EC PRIVATE KEY, conforme a ferramenta que gerou o par. A chave de API identifica a integração, e a assinatura com a chave privada prova que a requisição é sua: trate as duas como credenciais. A chave privada nunca é enviada à QI. Em produção, carregue a chave privada de um cofre de segredos ou variável de ambiente — nunca a deixe no código-fonte.
O passo a passo do portal, da criação da integração ao cadastro da chave pública, está em Integração pelo portal.
4. Defina os dados da requisição
# Troque pela URL do seu perfil e ambiente (tabela do passo 1).
base_url = "URL_BASE_DO_SEU_PERFIL" # ex.: "https://manager-api.sandbox.qidtvm.com.br"
method = "POST"
endpoint = "/authentication_test"
body = {"name": "QI Tech"}
O campo uri da assinatura deve ser idêntico ao caminho enviado na requisição. Se o endpoint receber parâmetros na query string, eles também fazem parte da URI assinada — veja Requisições com query string.
5. Monte o conteúdo da assinatura
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
dict_to_sign = {"timestamp": timestamp, "method": method, "uri": endpoint}
O timestamp segue o formato AAAA-MM-DDTHH:MM:SS, sem fuso e sem milissegundos, é sempre em UTC e é aceito com até 10 minutos de diferença do horário da QI. Gere um novo a cada requisição e mantenha o relógio do servidor sincronizado.
5.1. Se houver body, adicione o MD5
Requisições com body incluem o MD5 dos bytes exatos que serão enviados:
body_bytes = json.dumps(body).encode()
dict_to_sign["payload_md5"] = md5(body_bytes).hexdigest()
Envie exatamente esses body_bytes no passo 7. Se o JSON for serializado de novo — com outra ordem de campos, espaçamento ou acentuação —, o MD5 deixa de bater.
6. Assine o JWT
encoded_header_token = jwt.encode(
claims=dict_to_sign,
key=client_private_key,
algorithm=algorithm,
headers={"alg": algorithm, "typ": "JWT"},
)
O algorithm precisa ser da família da chave: RS* para RSA e ES* para EC. Se não for, o jwt.encode falha antes de a requisição sair, com um JWSError pouco descritivo — confira o algorithm primeiro.
7. Envie a requisição
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())
A resposta esperada é 200 com {"name": "QI Tech", "success": "Congrats!"}. O contrato de GET e POST /authentication_test está em Endpoints de teste.
Erros de autenticação
O code dos erros de autenticação termina com o mesmo número em todos os hosts. O prefixo indica qual host respondeu: MIT para manager-api, CIT para consultant-api e AIT para assignor-api. Se o prefixo não é o do seu perfil, a base_url está errada.
Final do code | Status | Erro | O que verificar |
|---|---|---|---|
000007 | 401 | Header ausente | Os dois headers, API-CLIENT-KEY e AUTHORIZATION, foram enviados. |
000008 | 401 | Chave de API inválida | A base_url é a do seu perfil e do ambiente certo, e a chave de API foi copiada sem espaços. |
000009 | 400 | Integração inativa | A integração está ativa. Fale com integracao.dtvm@qitech.com.br. |
000010 | 401 | Chave pública pendente | A chave pública já foi cadastrada na integração. |
000011 | 401 | Autenticação inválida | O JWT foi assinado com a chave privada que corresponde à chave pública cadastrada. |
000012 | 401 | Assinatura incompleta | O JWT tem os campos timestamp, method e uri. |
000013 | 401 | Método inválido | O method assinado é o mesmo da requisição, em maiúsculas. |
000014 | 401 | Assinatura expirada | O timestamp está em UTC, foi gerado na hora da requisição e o relógio do servidor está sincronizado. |
000015 | 401 | Endpoint inválido | O uri assinado é idêntico ao caminho enviado, incluindo a query string e sem a base_url. |
000016 | 401 | MD5 inválido | O payload_md5 foi calculado sobre os mesmos bytes enviados no body. |
000020 | 401 | Formato de timestamp | O timestamp está no formato AAAA-MM-DDTHH:MM:SS, sem fuso e sem milissegundos. Só manager-api e consultant-api devolvem este código: no assignor-api, timestamp fora do formato volta como erro 500. |
Erros de permissão, de rota e dos serviços estão em Erros da API.
Requisições com query string
Endpoints paginados ou com filtros recebem parâmetros na query string — por exemplo ?page=0&limit=20. Nesses casos, a query string faz parte da URI assinada.
O campo uri deve ser igual ao caminho enviado na requisição: mesmos parâmetros, na mesma ordem e com a mesma codificação. Se a URI assinada e a URI enviada não forem iguais, a requisição é recusada com status 401:
{
"title": "Invalid endpoint",
"description": "Invalid endpoint.",
"translation": "O endpoint é invalido",
"code": "MIT000015"
}
Exemplo
# base_url do passo 4. Este endpoint é da gestora (manager-api).
path = "/quota/fund_class/{fund_class_key}/investor_positions"
query_string = "page=0&limit=20"
# a query string faz parte da uri assinada
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())
Recomendações
- Monte a query string e envie a URL completa. Evite usar o argumento
paramsdorequestsjunto com uma URI já assinada: o cliente HTTP pode reordenar ou recodificar os parâmetros e invalidar a assinatura. - Requisições
GETnormalmente não têm corpo. Nesse caso, não envie body nem o campopayload_md5. - Valores com caracteres especiais (pontuação de documentos, datas, espaços) devem ser assinados já no formato final em que serão enviados na URL.