Skip to main content

Authentication test

Every request to the QI DTVM APIs carries two headers:

  • API-CLIENT-KEY: your integration's API key.
  • AUTHORIZATION: a JWT signed with your private key. QI validates the signature with the public key you registered in the key exchange.

This page builds both headers in Python and calls /authentication_test, which only responds 200 when host, API key and signature are correct. Run this test before any other call.

1. Choose your profile's base URL​

This is the most common error on the first call: copying the example with another profile's host.

Each profile has its own host. An API key is only recognized on the host of the profile it was issued for: a manager's key sent to the assignor host is rejected as Invalid API Key, even with a correct signature.

Your profileSandboxProduction
Manager (fund manager)https://manager-api.sandbox.qidtvm.com.brhttps://manager-api.qidtvm.com.br
Consultant (consultancy)https://consultant-api.sandbox.qidtvm.com.brhttps://consultant-api.qidtvm.com.br
Assignorhttps://assignor-api.sandbox.qidtvm.com.brhttps://assignor-api.qidtvm.com.br
Originatorhttps://originator-api.sandbox.qidtvm.com.brProvided by the integration team
Investorhttps://investor-api.sandbox.qidtvm.com.brhttps://investor-api.qidtvm.com.br
Distributorhttps://distributor-api.sandbox.qidtvm.com.brhttps://distributor-api.qidtvm.com.br

Use the profile QI provided when the integration was created — or, if you created the integration through the portal, the profile of the portal where it was registered. A Sandbox API key does not work in Production, and vice versa.

Managers and consultants

Copy the base URL straight from the portal: it is shown in the integration's Credenciais block, next to the API Key, already for the right profile and environment. See the image in step 3.

2. Install and import the libraries​

pip install "python-jose[cryptography]" requests
from datetime import datetime, timezone
import json
from hashlib import md5

from jose import jwt
import requests

3. Provide the API key, the private key and the algorithm​

Cryptography data
api_key = "YOUR_API_KEY"

client_private_key = """-----BEGIN PRIVATE KEY-----
YOUR_PRIVATE_KEY
-----END PRIVATE KEY-----"""

algorithm = "RS256" # your key's algorithm; see the table below

Where to find each value depends on how your integration was created:

ValueManagers and consultants (portal)Assignors, originators, investors and distributors
api_keyIn the portal, under Gestão de Acesso > Integração API (Access Management > API Integration): open the integration and copy the API Key field from the Credenciais (Credentials) block.Sent by the QI integration team after the key exchange.
client_private_keyThe qi-integration-...-private-key.pem file downloaded when generating the pair in the portal, or the private key of the pair you generated yourself.The private key of the pair you generated in the key exchange.
algorithmThe one shown in the portal below the chosen algorithm: RS256 for RSA (portal default), ES256, ES384 or ES512 for EC P-256, P-384 or P-521.ES512, for the EC P-521 key generated in the key exchange.

Credentials block of the integration screen in the portal, with the API Key and the API base URL highlighted

Paste the entire content of the private key file, with the BEGIN and END lines — the header may be PRIVATE KEY or EC PRIVATE KEY, depending on the tool that generated the pair. The API key identifies the integration, and the signature with the private key proves the request is yours: treat both as credentials. The private key is never sent to QI. In Production, load the private key from a secrets vault or an environment variable — never leave it in the source code.

The portal walkthrough, from creating the integration to registering the public key, is in Portal Integration.

4. Define the request data​

Request data
# Replace with the URL of your profile and environment (table in step 1).
base_url = "YOUR_PROFILE_BASE_URL" # e.g.: "https://manager-api.sandbox.qidtvm.com.br"

method = "POST"
endpoint = "/authentication_test"
body = {"name": "QI Tech"}
Attention

The signature's uri field must be identical to the path sent in the request. If the endpoint takes query string parameters, they are also part of the signed URI — see Requests with a query string.

5. Build the signature content​

Base dictionary
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")

dict_to_sign = {"timestamp": timestamp, "method": method, "uri": endpoint}

The timestamp follows the YYYY-MM-DDTHH:MM:SS format, with no time zone and no milliseconds, is always in UTC and is accepted with up to 10 minutes of difference from QI's clock. Generate a new one for each request and keep the server clock synchronized.

5.1. If there is a body, add the MD5​

Requests with a body include the MD5 of the exact bytes that will be sent:

Base dictionary
body_bytes = json.dumps(body).encode()

dict_to_sign["payload_md5"] = md5(body_bytes).hexdigest()

Send exactly these body_bytes in step 7. If the JSON is serialized again — with a different field order, spacing or accent encoding —, the MD5 no longer matches.

6. Sign the JWT​

encoded_header_token = jwt.encode(
claims=dict_to_sign,
key=client_private_key,
algorithm=algorithm,
headers={"alg": algorithm, "typ": "JWT"},
)

The algorithm must belong to the key's family: RS* for RSA and ES* for EC. Otherwise, jwt.encode fails before the request is sent, with a not very descriptive JWSError — check the algorithm first.

7. Send the request​

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())

The expected response is 200 with {"name": "QI Tech", "success": "Congrats!"}. The contract of GET and POST /authentication_test is in Test endpoints.

Authentication errors​

The code of authentication errors ends with the same number on every host. The prefix indicates which host responded: MIT for manager-api, CIT for consultant-api and AIT for assignor-api. If the prefix is not your profile's, the base_url is wrong.

code suffixStatusErrorWhat to check
000007401Missing headerBoth headers, API-CLIENT-KEY and AUTHORIZATION, were sent.
000008401Invalid API keyThe base_url is that of your profile and of the right environment, and the API key was copied without spaces.
000009400Inactive integrationThe integration is active. Contact integracao.dtvm@qitech.com.br.
000010401Public key pendingThe public key has already been registered on the integration.
000011401Invalid authenticationThe JWT was signed with the private key that matches the registered public key.
000012401Incomplete signatureThe JWT has the timestamp, method and uri fields.
000013401Invalid methodThe signed method is the same as the request's, in uppercase.
000014401Expired signatureThe timestamp is in UTC, was generated at request time and the server clock is synchronized.
000015401Invalid endpointThe signed uri is identical to the path sent, including the query string and without the base_url.
000016401Invalid MD5The payload_md5 was computed over the same bytes sent in the body.
000020401Timestamp formatThe timestamp is in the YYYY-MM-DDTHH:MM:SS format, with no time zone and no milliseconds. Only manager-api and consultant-api return this code: on assignor-api, a timestamp out of format comes back as a 500 error.

Permission, route and service errors are in API errors.

Requests with a query string​

Paginated or filtered endpoints take parameters in the query string — for example ?page=0&limit=20. In these cases, the query string is part of the signed URI.

The uri field must match the path sent in the request: same parameters, in the same order and with the same encoding. If the signed URI and the URI sent are not the same, the request is rejected with status 401:

Response
{
"title": "Invalid endpoint",
"description": "Invalid endpoint.",
"translation": "O endpoint é invalido",
"code": "MIT000015"
}

Example​

GET with pagination
# base_url from step 4. This endpoint belongs to the manager (manager-api).
path = "/quota/fund_class/{fund_class_key}/investor_positions"
query_string = "page=0&limit=20"

# the query string is part of the signed 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())

Recommendations​

  • Build the query string and send the full URL. Avoid using the requests params argument together with an already signed URI: the HTTP client may reorder or re-encode the parameters and invalidate the signature.
  • GET requests usually have no body. In that case, do not send a body or the payload_md5 field.
  • Values with special characters (document punctuation, dates, spaces) must be signed in the final form in which they will be sent in the URL.