Skip to main content

Receiving Webhooks

QI DTVM sends each webhook as a POST with a JSON body to the URL you configured. The request carries the SIGNATURE header, a JWT signed with HS256 using the Signature Key of your webhook configuration: a symmetric key known only to QI and to you.

Validate the signature​

import json
from hashlib import md5

from jose import jwt

signature_key = "YOUR_SIGNATURE_KEY"
webhook_url = "https://your-company.com/webhooks/qi" # the URL registered in the configuration


def validate_webhook(headers: dict, raw_body: bytes) -> dict:
# jwt.decode rejects the token if the signature does not match your Signature Key.
claims = jwt.decode(headers["SIGNATURE"], key=signature_key, algorithms=["HS256"])

if claims["uri"] != webhook_url:
raise ValueError("uri differs from the configured URL")
if claims.get("payload_md5") != md5(raw_body).hexdigest():
raise ValueError("payload_md5 does not match the received body")

return json.loads(raw_body)

The decoded JWT contains:

FieldDescription
timestampDate and time of the signature, in UTC, in the YYYY-MM-DDTHH:MM:SS format. Changes on every send.
methodHTTP method of the request: POST.
uriThe full destination URL configured for your webhook.
payload_md5MD5 hash of the body sent.

Compute the MD5 over the bytes received, before parsing the JSON. Re-encoding the JSON after parsing changes spacing and field order, and the hash no longer matches.

Body format​

Every webhook has the same envelope. The content of data depends on the event and is described on each product's webhooks page.

{
"webhook_type": "settlement.payment_batch_status_change",
"webhook_datetime": "2026-10-07T14:32:10Z",
"data": {}
}
FieldDescription
webhook_typeEvent, in the <product>.<event> format.
webhook_datetimeDate and time the event was generated. It is the same across all attempts.
dataEvent data: an object or a list, depending on the event.
Attention!

Do not map webhooks restrictively: new fields may be added to the envelope and to data at any time.

AGENT-KEY header​

The AGENT-KEY header carries the key of the agent that owns the webhook configuration, that is, your key: the manager's manager_key, the consultant's consultant_key or the assignor's assignor_key. It does not identify the fund. To know which fund the event refers to, use the fields in data, such as fund_class_key.

Delivery and attempts​

  • A delivery counts as successful when your URL responds with an HTTP status lower than 300 within 30 seconds. Any other status, timeout or network error counts as a failure.
  • Each event is sent up to 4 times. After the 4th failure, it is marked as failed and is no longer resent automatically.
  • Respond quickly: store the webhook and process it later. The time your application takes to process counts toward the 30 seconds.
A webhook that never arrived

A webhook that exhausted its attempts can be resent by QI DTVM. Contact integracao.dtvm@qitech.com.br stating the period and the event type. Resending is not an operation available in your integration.

Duplicates​

You may receive the same webhook more than once, for two reasons:

  1. Your application received the webhook, but the response did not reach QI in time. The send is repeated.
  2. The same event may have to be delivered to more than one agent: for example, to the fund's manager, consultant and assignor. These sends are attempted together and in sequence. If the URL of any of them fails, the whole attempt is repeated, and those who had already received it receive it again.

The second case means that a failure at another agent's URL produces duplicates at yours. Treat reception as idempotent: the repetition arrives with the same body, byte for byte (the same webhook_type, webhook_datetime and data), and therefore the same payload_md5. Store the payload_md5 of webhooks already processed and discard repeats. Do not use the header JWT for this: the timestamp changes on every send.

Origin validation​

In addition to validating the signature, you can restrict your URL to QI's outbound IPs:

EnvironmentIP
Production54.205.166.229
Sandbox52.72.221.4