# QI Tech — Insurance-as-a-Service › Primeiros Passos

Documentação da QI Tech em texto corrido, para colar em um LLM.
Fonte: https://docs.qitech.com.br
9 página(s).

Índice:
- Autenticação (/documentation/seguros/introducao/autenticacao)
- Introdução (/documentation/seguros/introducao/inicio)
- Configurando webhooks (/documentation/seguros/primeiros_passos/seguros_configurando_webhooks)
- Configurar IP de Integração (/documentation/seguros/primeiros_passos/seguros_configurar_ip_de_integracao)
- Troca de chaves (/documentation/seguros/primeiros_passos/seguros_troca_de_chaves)
- Possíveis erros (/documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_possiveis_erros)
- Exemplo completo de teste de autenticação (/documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_teste_de_autenticacao_completo)
- Teste de autenticação (/documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_teste_de_autenticacao_v2)
- Validação de webhooks (/documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_webhook_v2)

---

# Autenticação

URL: /documentation/seguros/introducao/autenticacao

A autenticação do Insurance-as-a-Service segue o **padrão de requisição assinada da QI Tech** — o mesmo utilizado no Lending-as-a-Service, no Banking-as-a-Service e na QI DTVM. Se você já integra qualquer outra linha de produto QI Tech, o mecanismo é idêntico; muda apenas o host.

## Requisição assinada

Todas as requisições devem usar **HTTPS** com **TLS 1.2 ou 1.3** e conter dois headers:

| Header           | Descrição                                                                                    |
| ---------------- | -------------------------------------------------------------------------------------------- |
| `API-CLIENT-KEY` | Chave disponibilizada pelo time de Integração da QI Tech que identifica a sua integração.     |
| `AUTHORIZATION`  | Assinatura da requisição no padrão JWT, gerada com a sua chave privada.                       |

A QI Tech utiliza o padrão de chaves assimétricas: você gera um par de chaves, assina cada requisição com a sua **chave privada** e a QI Tech valida a assinatura com a sua **chave pública**.

O passo a passo completo — geração do par de chaves, envio da chave pública à QI Tech e montagem do header `AUTHORIZATION` — está descrito em:

- [Troca de Chaves](/documentation/seguros/primeiros_passos/seguros_troca_de_chaves)
- [Teste de Autenticação](/documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_teste_de_autenticacao_v2)
- [Exemplo Completo de Autenticação](/documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_teste_de_autenticacao_completo)
- [Possíveis Erros de Autenticação](/documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_possiveis_erros)

:::caution Atenção
A chave privada é de uso exclusivo do parceiro integrador e deve ser armazenada com segurança. A QI Tech nunca irá pedir, em hipótese alguma, que você a compartilhe.
:::

Opcionalmente, você pode restringir as chamadas da sua integração a uma lista de endereços IP autorizados. Veja [Configurar IP de Integração](/documentation/seguros/primeiros_passos/seguros_configurar_ip_de_integracao).

## Recebimento de webhooks

Os eventos de pedido e de apólice são notificados por webhooks assinados pela QI Tech, na URL de webhook configurada para a sua integração. A estrutura dos webhooks, os headers e a forma de responder estão em [Configurando webhooks](/documentation/seguros/primeiros_passos/seguros_configurando_webhooks); a verificação da assinatura, em [Validação de webhooks](/documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_webhook_v2).

---

# Introdução

URL: /documentation/seguros/introducao/inicio

O **Insurance-as-a-Service** da QI Tech permite que parceiros distribuam produtos de seguro por API: consulta do catálogo de produtos habilitados, cotação, venda (pedido com aceite e pagamento do segurado), acompanhamento das apólices emitidas e do repasse financeiro das comissões.

Essa documentação descreve os fluxos, endpoints e estruturas de dados da jornada completa de distribuição de seguros.

Obs.: Em caso de dúvidas em qualquer etapa do processo, favor entrar em contato com [api@qitech.com.br](mailto:api@qitech.com.br) detalhando seu problema/dúvida que te auxiliaremos.

## Visão geral da jornada

1. **Catálogo** — consulte os produtos de seguro que a sua integração está habilitada a distribuir, com as coberturas, limites e a sua faixa de comissão ([Catálogo de Produtos](/documentation/seguros/catalogo/inicio)).
2. **Cotação** — precifique uma seleção de produtos e coberturas para um cliente, sem criar nenhum recurso ([Criar cotação](/documentation/seguros/cotacao/criar_cotacao)), e descubra a faixa de preço vendável de cada produto ([Simular faixa de preço](/documentation/seguros/cotacao/simular_precos)).
3. **Pedido** — submeta a venda, já com o **aceite** que você coletou do segurado. O pedido nasce aguardando o **pagamento** da primeira parcela; confirmado o pagamento, o pedido é emitido ([Pedidos](/documentation/seguros/pedidos/inicio)).
4. **Apólices** — a emissão do pedido gera uma apólice por produto vendido, emitida junto à seguradora. Consulte e cancele apólices individualmente ([Apólices](/documentation/seguros/apolices/inicio)). _Superfície ainda não publicada._
5. **Financeiro** — acompanhe o seu saldo de comissão, o extrato de movimentações e os repasses realizados ([Financeiro](/documentation/seguros/financeiro/inicio)). _Superfície ainda não publicada._

Cada etapa relevante notifica a sua URL de callback por [webhooks de pedido](/documentation/seguros/pedidos/webhooks) e [webhooks de apólice](/documentation/seguros/apolices/webhooks).

## Ambientes (Hosts)

O Insurance-as-a-Service possui dois ambientes, SANDBOX e PRODUÇÃO. Ambos possuem comportamento idêntico, porém o ambiente de SANDBOX opera com valores e emissões totalmente fictícios, enquanto o de Produção realiza transações e emissões válidas.

| Ambiente | Host                                       |
| -------- | ------------------------------------------ |
| Sandbox  | https://api.sandbox.insurance.qitech.app   |
| Produção | https://api.insurance.qitech.app           |

:::danger Aviso Importante!
Não devem ser usados dados reais de pessoas físicas e/ou jurídicas nos ambientes de Sandbox da QI Tech.
:::

## Convenções da API

- Os paths são versionados com o segmento de versão **liderando** o caminho (ex.: `/v1/insurance/order`, `/v1/product_catalog/products`).
- Todos os recursos são endereçados por chaves públicas UUID (`order_key`, `policy_key`, `product_key`), nunca por identificadores numéricos internos.
- Os paths de coleção nem sempre são plurais: a criação e a listagem de pedidos vivem em `/v1/insurance/order` (singular), enquanto as rotas por chave usam `/v1/insurance/orders/{order_key}`. Siga o path indicado na página de cada endpoint.
- Valores monetários trafegam como **número JSON com 2 casas decimais** (ex.: `312.48`, nunca string), sempre em BRL e sempre **brutos** (com IOF). Taxas e percentuais são números com 4 casas em `(0, 1]` (ex.: `0.1000`). Identificadores numéricos com zeros à esquerda significativos (documentos, códigos de ramo) permanecem strings.
- Status são strings de enumerador em caixa baixa (ex.: `awaiting_payment`, `emitted`). Não mapeie o conjunto de forma fechada — veja os [status reservados](/documentation/seguros/pedidos/inicio).
- Vigências são objetos aninhados `term: { "start_date", "end_date" }` — por produto e por cobertura.
- Datas seguem `AAAA-MM-DD` e data-hora segue ISO 8601 (`2026-07-16T14:03:22Z`).
- Listagens usam paginação por offset, mas **a forma varia por superfície** — confira sempre a página do endpoint. Pedidos usam `page` / `page_size` (padrão `50`, máximo `200`) e devolvem `{ items, page, page_size, total }`; o catálogo de produtos usa `page` / `rows_per_page` (padrão `50`) e devolve `{ data, pagination { current_page, next_page, rows_per_page } }`, sem `total`.
- Todo erro (`non-2xx`) retorna o corpo padrão `{ "title", "description", "translation", "code" }`. O `code` é o **único** campo para tratamento programático — `title`/`description`/`translation` podem mudar sem aviso.
- O escopo de acesso é sempre o da sua integração: uma chave de outro parceiro é indistinguível de uma chave inexistente e retorna `404`.

## Para começar

A autenticação segue o padrão QI Tech de requisições assinadas, o mesmo utilizado nas demais linhas de produto. Veja [Autenticação](/documentation/seguros/introducao/autenticacao).

Antes de consumir os endpoints desta documentação, complete os passos abaixo **em ambiente de sandbox**:

1. Entrar em contato com o time de Integração ([api@qitech.com.br](mailto:api@qitech.com.br)) para iniciar o onboarding da sua integração.
2. [Gerar o par de chaves e enviar a sua chave pública por meio seguro](/documentation/seguros/primeiros_passos/seguros_troca_de_chaves) para receber as credenciais de integração.
3. [Realizar o teste de autenticação](/documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_teste_de_autenticacao_v2).
4. [Configurar a URL de recebimento de webhooks](/documentation/seguros/primeiros_passos/seguros_configurando_webhooks).

---

# Configurando webhooks

URL: /documentation/seguros/primeiros_passos/seguros_configurando_webhooks

:::info Veja também

- [Validação de webhooks](/documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_webhook_v2)
- [Configurar IP de integração](/documentation/seguros/primeiros_passos/seguros_configurar_ip_de_integracao)

:::

A QI Tech notifica as mudanças de status dos seus pedidos e apólices por `POST` na URL de webhook configurada para a sua integração. Cada webhook é assinado pela QI Tech e deve ser validado antes de ser processada.

## Eventos disponíveis

| `webhook_type` | Quando é enviado | Detalhes |
|---|---|---|
| `insurance.order.status_changed` | A cada mudança de status do pedido: criado, recusado, emitido, expirado ou cancelado antes da emissão. | [Webhooks do pedido](/documentation/seguros/pedidos/webhooks) |
| `insurance.policy.status_changed` | Quando a apólice é emitida pela seguradora e quando é cancelada. | [Webhooks da apólice](/documentation/seguros/apolices/webhooks) |

## Como configurar

No Insurance-as-a-Service a configuração dos webhooks **não está disponível pelo portal QI Tech**. Ela é feita pelo time de Integração da QI Tech.

Envie para [api@qitech.com.br](mailto:api@qitech.com.br):

- A **URL de webhook**, por ambiente (sandbox e produção). A URL deve ser HTTPS, aceitar `POST`, ter no máximo 200 caracteres e não conter fragmento (`#...`).
- Os **headers** que você precisa que a QI Tech envie em cada webhook, se houver — por exemplo um header de autenticação do seu lado.

Uma mesma URL recebe todos os eventos. Se você precisar de uma URL diferente para cada tipo de evento, informe no pedido.

Em retorno, você recebe a **chave pública da QI Tech** (PEM), exclusiva da sua integração, que valida a assinatura de cada webhook.

Avise-nos com antecedência quando a URL mudar: enquanto a alteração não for registrada, os webhooks continuam sendo enviados para o endereço anterior.

## Formato dos webhooks

Todo webhook do Insurance-as-a-Service tem a mesma estrutura:

| Campo | Tipo | Descrição |
|---|---|---|
| `webhook_type` | string | Tipo do evento. Veja a tabela de [eventos disponíveis](#eventos-disponiveis). |
| `status` | string | Novo status do pedido ou da apólice. |
| `key` | string | Chave do recurso notificado: o `order_key` nos eventos de pedido, o `policy_key` nos de apólice. |
| `event_datetime` | string | Data e hora do evento, em UTC, no formato ISO 8601 com precisão de segundos. |
| `data` | object | Dados do evento. Os atributos de cada tipo estão nas páginas de detalhe. |

```json title="Exemplo de webhook"
{
  "status": "emitted",
  "webhook_type": "insurance.order.status_changed",
  "key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
  "event_datetime": "2026-10-05T15:59:34Z",
  "data": {
    "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
    "requester_key": "3f1c9a2e-4b7d-4e0a-9c55-2d8e7f6a1b30",
    "status": "emitted",
    "customer_document_number": "96969879003",
    "reason": null,
    "product_count": 1,
    "sequence": 2,
    "schema_version": 1
  }
}
```

### Headers

| Header | Descrição |
|---|---|
| `AUTHORIZATION` | JWT ES512 com a assinatura da QI Tech. Se você configurou um header próprio chamado `AUTHORIZATION`, a assinatura chega em `QI-AUTHORIZATION`. |
| `API-CLIENT-KEY` | Chave que identifica a sua integração. |
| `webhook_key` | Identificador único do webhook. Um webhook reenviado mantém o mesmo valor. |
| `webhook-type` | O mesmo `webhook_type` do corpo. |
| `Content-Type` | `application/json`. |

Os nomes de header não diferenciam maiúsculas de minúsculas: dependendo do seu servidor, eles podem chegar como `Authorization` ou `Api-Client-Key`.

## Como responder

Responda com o status `200`, `201`, `202` ou `204` em até **10 segundos**. Qualquer outro status, ou a falta de resposta nesse prazo, conta como falha de entrega.

A entrega é do tipo *best-effort*: um webhook que falhou não é reenviado automaticamente. Não dependa exclusivamente dos webhooks — o estado atual de um pedido ou de uma apólice pode sempre ser consultado em [`GET /v1/insurance/orders/{order_key}`](/documentation/seguros/pedidos/consultar_pedido) e [`GET /v1/insurance/policies/{policy_key}`](/documentation/seguros/apolices/consultar_apolice).

## Ordem e duplicidade

Os webhooks **não chegam necessariamente na ordem em que os eventos aconteceram**. Para saber qual é a mais recente, use `data.sequence`: dentro de um mesmo pedido ou de uma mesma apólice, um valor maior é sempre um evento posterior. A sequência não é contínua — não conte com valores consecutivos.

Para descartar um webhook já processado, use o header `webhook_key`.

:::danger Atenção!

Os webhooks da QI Tech não devem ser mapeados de forma estrita.
Campos adicionais podem ser incluídos aos payloads dos webhooks retornados em nossas APIs.

:::

---

# Configurar IP de Integração

URL: /documentation/seguros/primeiros_passos/seguros_configurar_ip_de_integracao

:::info Veja também
- [Configurando Webhooks](/documentation/seguros/primeiros_passos/seguros_configurando_webhooks)
- [Troca de Chaves](/documentation/seguros/primeiros_passos/seguros_troca_de_chaves)
:::

As chamadas à API do Insurance-as-a-Service são restritas a uma lista de **endereços IP públicos** previamente autorizados para a sua integração. Uma requisição originada de um IP fora da lista é recusada, por mais correta que seja a sua assinatura.

## Como configurar

No Insurance-as-a-Service o cadastro dos IPs **não está disponível pelo portal QI Tech**. A lista é registrada pelo time de Integração da QI Tech.

Envie a relação dos endereços IP públicos de onde a sua integração fará as chamadas para [api@qitech.com.br](mailto:api@qitech.com.br), informando o ambiente (sandbox ou produção) de cada endereço.

O que é aceito em cada entrada:

- Um endereço IPv4 público, individual (ex.: `189.10.20.30`)
- Um endereço IPv6 público, individual

Cada endereço é comparado exatamente como foi cadastrado, portanto informe **um endereço por entrada** — inclusive quando eles forem vizinhos na mesma faixa.

:::caution Envie a lista completa desde o início
Informe todos os endereços de uma vez no onboarding da sua integração. Incluir um endereço depois exige uma nova solicitação ao time de Integração, e até que ela seja processada as chamadas partindo dele são recusadas.

Avise-nos **antes** de passar a chamar a API a partir de uma rede nova — um novo escritório, uma VPN, um NAT gateway adicional ou um _runner_ de CI. O cadastro passa a valer imediatamente após ser registrado, mas só depois de ser registrado.
:::

## Possíveis erros

Todo erro (`non-2xx`) retorna o corpo padrão de erro — trate programaticamente **apenas** o campo `code`.

| Status | Código | Descrição |
|---|---|---|
| `403` | `EGW000007` | O IP de origem da requisição não está na lista de IPs permitidos da integração. |

```json
{
  "title": "Forbidden",
  "description": "The request source IP is not in the integration's allowed IP list.",
  "translation": "O IP de origem da requisição não está na lista de IPs permitidos da integração.",
  "code": "EGW000007",
  "extra_fields": {}
}
```

Se você receber esse erro, confirme qual endereço a sua infraestrutura está de fato usando para sair (o IP público de saída pode não ser o do servidor, no caso de NAT, proxy ou balanceador) e compare com a lista que você nos enviou.

---

# Troca de chaves

URL: /documentation/seguros/primeiros_passos/seguros_troca_de_chaves

## Conferindo o formato da chave pública

A chave pública que esperamos receber é o arquivo gerado pelo **segundo** comando da seção anterior (`openssl ec ... -pubout`) — `jwtECDSASHA512.key.pub` no Unix e no Windows, `ec512-public.pem` no Mac OS. Ele está no formato **PEM**, em várias linhas, delimitado por `-----BEGIN PUBLIC KEY-----` e `-----END PUBLIC KEY-----`, como no exemplo abaixo — que serve apenas para conferir o formato e nunca deve ser cadastrado como a sua chave:

```text
-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAD8a66B1olkMDoeQM9imiOOCuq1Hq
LOq0bu6ry2GJJzDtjGws5u52SQikFFv0YSRSGpgAJN7cZiuXQkGooDxDvXsAev+2
iQn7HImrLN8YaNPqGMU28UFiuc2SSPf5QdHozEf6LRqq1bhg2oJirpLJAgKlse9M
hhsYr9sznWJDoLOJgjM=
-----END PUBLIC KEY-----
```

Confira o arquivo antes de nos enviar:

```bash
openssl ec -pubin -in jwtECDSASHA512.key.pub -text -noout
```

A resposta esperada termina em `ASN1 OID: secp521r1` e `NIST CURVE: P-521`. Se o comando responder um erro de leitura, o arquivo não é uma chave pública em PEM.

### Se a sua chave está no formato OpenSSH

O `ssh-keygen` do primeiro comando também grava um arquivo `.pub` ao lado da chave privada, mas ele está no formato **OpenSSH**: uma única linha começando pelo tipo da chave e terminando no comentário.

```text
ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAI... usuario@maquina
```

Esse arquivo corresponde ao mesmo par de chaves, mas **não é o formato aceito no cadastro da sua integração**. Você não precisa gerar um novo par: basta executar o comando `openssl ec ... -pubout` sobre a sua chave privada para obter a mesma chave pública em PEM.

## Envio da chave pública

As suas requisições à API são assinadas com a sua chave privada, e por isso você nos fornece a sua chave pública. Em retorno, fornecemos uma chave única do tipo UUID (`API-CLIENT-KEY`) que representa a sua integração via API dentro do nosso sistema.

No sentido inverso, os webhooks que a QI Tech envia são assinados por nós. A **chave pública da QI Tech**, usada para validá-los, é entregue na [configuração dos webhooks](/documentation/seguros/primeiros_passos/seguros_configurando_webhooks).

No Insurance-as-a-Service o cadastro da chave pública ainda não está disponível pelo portal QI Tech. Envie a sua **chave pública em PEM** — o arquivo conferido na seção anterior — ao time de Integração da QI Tech por um **meio seguro** — entre em contato com [api@qitech.com.br](mailto:api@qitech.com.br) para alinhar o canal de envio. Em retorno, você receberá a sua **chave de integração** (`API-CLIENT-KEY`).

:::danger Atenção!

Nunca compartilhe sua chave privada, ela é de uso exclusivo seu e o compartilhamento da mesma no lugar da chave pública compromete a segurança de suas requests. Além disso, não compartilhe sua chave pública QI Tech e chave de integração pois eles são seu meio de comunicação com nossas APIs.

:::

---

# Possíveis erros

URL: /documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_possiveis_erros

:::info Veja também
- [Teste de autenticação](./seguros_teste_de_autenticacao_v2)
- [Exemplo completo de autenticação](./seguros_teste_de_autenticacao_completo)
:::

## Erro no token

Caso a assinatura da string_to_sign esteja incorreta, um erro será apresentado relativo ao encoded_header_token :

STATUS 401

Response Body

```json
{
	"title": "QI Unauthenticated",
	"description": "Please provide valid credentials as part of the request. (Documentation: https://qitech.com.br/documentation) Details: Failed while decoding the authentication token",
	"translation": "Por favor forneça credenciais válidas como parte da request. (Documentação: https://qitech.com.br/documentation) Detalhes: Falha ao decodificar o token de autenticação",
	"code": "GDF000014"
}
```

## Erro no <strong>API_KEY</strong>

Caso a API_KEY não seja enviada no header o seguinte erro será apresentado:

STATUS 400

Response Body

```json
{
	"title": "Bad Request",
	"description": "No API Client Key received",
	"translation": "Nenhuma chave de API do cliente recebida",
	"code": "GDF000003"
}
```

## <strong>API_KEY</strong> incorreta

Caso a API_KEY enviada não corresponda a API_KEY apresentada no front QI Tech após o cadastro de chaves o seguinte retorno será apresentado:

STATUS 404

Response Body

```json
  {
  	"code": "GDF000018",
  	"title": "Not Found",
  	"description": "No ClientIntegration found for api_client_key: {api_client_key}.",
  	"translation": "Nenhuma ClientIntegration encontrada para api_client_key: {api_client_key}."
  }
```

## Endpoint não autorizado

Caso o endpoint ou método acessado não esteja autorizado o seguinte erro será retornado:

STATUS 401

Response Body

```json
{
	"title": "QI Unauthenticated",
	"description": "Please provide valid credentials as part of the request. (Documentation: https://qitech.com.br/documentation) Details: Endpoint or HTTP method not allowed for the given ClientIntegration (Action: POST /debt)",
	"translation": "Por favor forneça credenciais válidas como parte da request. (Documentação: https://qitech.com.br/documentation) Detalhes: Endpoint ou método HTTP não permitido para a ClientIntegration fornecida (Action: POST /debt)",
	"code": "GDF000014"
}
```

:::caution Atenção!

Para requisitar acesso ao endpoint que retornou o erro referido, é necessário solicitar a liberação ao time de suporte QI Tech.
:::

---

# Exemplo completo de teste de autenticação

URL: /documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_teste_de_autenticacao_completo

:::info Veja também
- [Teste de autenticação passo a passo](./seguros_teste_de_autenticacao_v2)
- [Possíveis erros](./seguros_possiveis_erros)
:::

### Visão Geral
Esta documentação detalha o processo de assinatura e encriptação de cabeçalhos para autenticação segura em requisições à nossa API. O processo garante que as requisições sejam confiáveis e seguras, prevenindo acessos não autorizados e garantindo a integridade dos dados.

:::caution Atenção!

Requisições dos métodos `GET` e `DELETE` não possuem corpo. O hash md5 dessas requisições deve ser gerado sobre a **string vazia** (`""`), e o valor é sempre a constante abaixo:

```
d41d8cd98f00b204e9800998ecf8427e
```

Esse valor é **diferente** do utilizado no Lending-as-a-Service, que assina `GET` e `DELETE` com o md5 do objeto JSON vazio (`"{}"`), `99914b932bd37a50b983c5e7c90ae93b`. Se você já integra o Lending-as-a-Service, não reaproveite a constante: assinar um `GET` do Insurance-as-a-Service com ela retorna `401` com o código `EGW000006`.
:::

### Os dois erros `401` do header `AUTHORIZATION`

Uma falha no header `AUTHORIZATION` retorna `401` com um de dois códigos, e eles apontam para causas diferentes:

| Status | Código | Descrição |
|---|---|---|
| `401` | `EGW000004` | A assinatura da requisição não pôde ser verificada com a chave pública registrada da integração. |
| `401` | `EGW000006` | O digest do payload assinado não corresponde ao corpo da requisição. |

- `EGW000004` significa que o problema está no **par de chaves**: a assinatura não foi verificada com a chave pública registrada para a sua integração. Confira a [troca de chaves](/documentation/seguros/primeiros_passos/seguros_troca_de_chaves), inclusive o formato do arquivo enviado.
- `EGW000006` significa que a assinatura **foi verificada com sucesso** — a sua chave está correta — e que apenas o `md5` não corresponde ao corpo enviado. Não investigue o par de chaves: confira o hash, começando pela constante de `GET` e `DELETE` acima.

**Python**

```python
#Aqui, importamos as bibliotecas necessárias ao longo do processo de autenticação.
from jose import jwt
import json
from datetime import datetime, timezone
from hashlib import md5
import requests

def get_auth_header(endpoint, method, CLIENT_PRIVATE_KEY, API_KEY, request_body=None):

    if request_body is None:
        request_body = {}

    #O objeto de data e hora informado deve estar em UTC e deve seguir o padrão da norma internacional ISO 8601 ("2023-06-26T19:48:32.759844Z")
    timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")

    #Definimos o algoritmo de codificação JWT
    jwt_header = {
        "typ": "JWT",
        "alg": "ES512"
    }

    #Construir hash em MD5 para assinatura no cabeçalho (header) utilizando o payload
    json_body = json.dumps(request_body)
    md5_hash = md5(json_body.encode()).hexdigest()

    #Essas são as infromações necessárias para assinatura do cabeçalho
    jwt_body = {
        "payload_md5": md5_hash,
        "timestamp": timestamp,
        "method": method,
        "uri": endpoint
    }

    #Realizar criptografia do header
    encoded_header_token = jwt.encode(
        claims=jwt_body,
        key=CLIENT_PRIVATE_KEY,
        algorithm="ES512",
        headers=jwt_header
    )

    #Montar header assinado
    signed_header = {
        "AUTHORIZATION": encoded_header_token,
        "API-CLIENT-KEY": API_KEY
    }

    return signed_header

if __name__ == "__main__":

    #Utilizaremos as variáveis base_url, endpoint, method e request_body. Neste exemplo faremos um POST no endpoint "/test".
    #As chaves contidas neste exemplo são apenas para fins de demonstração. Por favor, utilize suas próprias chaves.
    CLIENT_PRIVATE_KEY = "SUA PRIVATE KEY AQUI"
    API_KEY = "SUA API KEY AQUI"

    BASE_URL = "https://api.sandbox.insurance.qitech.app"
    METHOD = "POST" #GET ou POST
    REQUEST_BODY = {
        "name": "QI Tech"
    }

    #Para fazer um GET na /test, é necessário inserir a API key ao final, enquanto para fazer um POST no mesmo endpoint, não é necessário
    if METHOD == 'GET':
        ENDPOINT = f"/test/{API_KEY}"
        signed_header = get_auth_header(ENDPOINT, METHOD, CLIENT_PRIVATE_KEY, API_KEY)
        response = requests.get(f"{BASE_URL}{ENDPOINT}", headers=signed_header)
    else:
        ENDPOINT = f"/test"
        signed_header = get_auth_header(ENDPOINT, METHOD, CLIENT_PRIVATE_KEY, API_KEY, REQUEST_BODY)
        response = requests.post(f"{BASE_URL}{ENDPOINT}", json=REQUEST_BODY, headers=signed_header)

    print(response.status_code)
    print(response.json())
```

**PHP**

```php
<?php
require __DIR__ . '/vendor/autoload.php';

//Aqui, importamos as bibliotecas necessárias ao longo do processo de autenticação.
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Signature\JWSTokenSupport;
use Jose\Component\Signature\Algorithm\ES512;
use Jose\Component\Signature\Serializer\CompactSerializer;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\JWSBuilder;

function get_auth_header($endpoint, $method, $privateKeyString, $api_key, $request_body = null) {

    if ($request_body === null) {
        $request_body = (object)[];
    }

    //O objeto de data e hora informado deve estar em UTC e deve seguir o padrão da norma internacional ISO 8601 ("2023-06-26T19:48:32.759844Z")
    $microtime_float = microtime(true);
    $datetime = new DateTimeImmutable('@' . floor($microtime_float), new DateTimeZone('UTC'));
    $timestamp = $datetime->format('Y-m-d\TH:i:s.') . sprintf('%06d', ($microtime_float - floor($microtime_float)) * 1000000) . 'Z';

    //Definimos o algoritmo de codificação JWT
    $header = [
        "typ" => "JWT",
        "alg" => "ES512"
    ];

    //Construir hash em MD5 para assinatura no cabeçalho (header) utilizando o payload
    $request_body_json = json_encode($request_body);
    $md5_hash = md5($request_body_json);

    //Essas são as infromações necessárias para assinatura do cabeçalho
    $payload = [
        "payload_md5" => $md5_hash,
        "timestamp" => $timestamp,
        "method" => $method,
        "uri" => $endpoint
    ];

    // Inicializar Algorithm Manager com ES512
    $algorithmManager = new AlgorithmManager([
        new ES512(),
    ]);

    // Inicializar JWS Builder
    $jwsBuilder = new JWSBuilder(
        $algorithmManager,
        new JWSTokenSupport()
    );

    $privateKey = JWKFactory::createFromKey($privateKeyString);

    //Realizar criptografia do header
    $jws = $jwsBuilder
        ->create()
        ->withPayload(json_encode($payload))
        ->addSignature($privateKey, $header)
        ->build();

    $serializer = new CompactSerializer();
    $jwt = $serializer->serialize($jws, 0);

    //Montar header assinado
    $headers = [
        'Authorization' => $jwt,
        'API-CLIENT-KEY' => $api_key,
    ];

    return $headers;
}

if (php_sapi_name() == 'cli' || (isset($_SERVER['REQUEST_METHOD']) && realpath($_SERVER['SCRIPT_FILENAME']) === __FILE__)) {
    
    //Utilizaremos as variáveis base_url, endpoint, method e request_body. Neste exemplo faremos um POST no endpoint "/test".
    $base_url = "https://api.sandbox.insurance.qitech.app";
    $method = "POST"; // HTTP method: "GET" or "POST"

    $request_body = ["name" => "QI Tech"];

    //As chaves contidas neste exemplo são apenas para fins de demonstração. Por favor, utilize suas próprias chaves.
    $api_key = "SUA API KEY AQUI";
    $privateKeyString = "SUA PRIVATE KEY AQUI";

    $response = null;

    #Para fazer um GET na /test, é necessário inserir a API key ao final, enquanto para fazer um POST no mesmo endpoint, não é necessário
    if ($method == 'GET') {
        $endpoint = "/test/" . $api_key;
        $headers = get_auth_header($endpoint, $method, $privateKeyString, $api_key);
        $url = $base_url . $endpoint;
        $response = \WpOrg\Requests\Requests::get($url, $headers);
    } else {
        $endpoint = "/test";
        $headers = get_auth_header($endpoint, $method, $privateKeyString, $api_key, $request_body);
        $url = $base_url . $endpoint;
        $response = \WpOrg\Requests\Requests::post($url, $headers, json_encode($request_body));
    }

    if ($response) {
        echo "HTTP Status Code: " . $response->status_code . "\n";

        $json_response = json_decode($response->body, true);
        if (json_last_error() === JSON_ERROR_NONE) {
            echo "Response JSON:\n";
            print_r($json_response);
        } else {
            echo "Error decoding JSON. Raw Response Text:\n";
            echo $response->body . "\n";

    }
}
?>
```

**Node.js**

```js
//Aqui, importamos as bibliotecas necessárias ao longo do processo de autenticação.
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const axios = require('axios');

function getAuthHeader(endpoint, method, client_private_key, api_key, request_body = null) {
    if (request_body === null) {
        request_body = {};
    }

    //O objeto de data e hora informado deve estar em UTC e deve seguir o padrão da norma internacional ISO 8601 ("2023-06-26T19:48:32.759844Z")
    const now = new Date();
    const isoString = now.toISOString();
    const timestamp = isoString.slice(0, -1) + (now.getMilliseconds() * 1000).toString().padStart(6, '0').slice(0, 3) + 'Z';

    //Definimos o algoritmo de codificação JWT
    const jwt_header = {
        typ: 'JWT',
        alg: 'ES512'
    };

    //Construir hash em MD5 para assinatura no cabeçalho (header) utilizando o payload
    const str_body = JSON.stringify(request_body);
    const md5_hash = crypto.createHash('md5').update(str_body).digest('hex');

    //Essas são as infromações necessárias para assinatura do cabeçalho
    const jwt_body = {
        payload_md5: md5_hash,
        timestamp: timestamp,
        method: method,
        uri: endpoint
    };

    // Inicializar JWS Builder
    const encoded_header_token = jwt.sign(
        jwt_body,
        client_private_key,
        {
            algorithm: 'ES512',
            header: jwt_header
        }
    );

    //Realizar criptografia do header
    const signed_header = {
        'AUTHORIZATION': encoded_header_token,
        'API-CLIENT-KEY': api_key
    };

    return signed_header;
}
//Utilizaremos as variáveis BASE_URL, ENDPOINT, METHOD e REQUEST_BODY. Neste exemplo faremos um POST no endpoint "/test".
async function main() {
    const BASE_URL = "https://api.sandbox.insurance.qitech.app";
    const METHOD = "POST"; //"POST" ou "GET"

    const REQUEST_BODY = {
        name: "QI Tech"
    };

    const API_KEY = "SUA API KEY AQUI";
    const CLIENT_PRIVATE_KEY = "SUA PRIVATE KEY AQUI";

    let ENDPOINT;
    let url;
    let signed_header;

    try {
    //Para fazer um GET na /test, é necessário inserir a API key ao final, enquanto para fazer um POST no mesmo endpoint, não é necessário
        if (METHOD === 'GET') {
            ENDPOINT = `/test/${API_KEY}`;
            url = `${BASE_URL}${ENDPOINT}`;
            signed_header = getAuthHeader(ENDPOINT, METHOD, CLIENT_PRIVATE_KEY, API_KEY);

            const response = await axios.get(url, { headers: signed_header });
            console.log("Status Code:", response.status);
            console.log("Response Body:", response.data);

        } else {
            ENDPOINT = "/test";
            url = `${BASE_URL}${ENDPOINT}`; // Corrected string interpolation
            signed_header = getAuthHeader(ENDPOINT, METHOD, CLIENT_PRIVATE_KEY, API_KEY, REQUEST_BODY);

            const response = await axios.post(url, REQUEST_BODY, { headers: signed_header });
            console.log("Status Code:", response.status);
            console.log("Response Body:", response.data);
        }
    } catch (error) {
        // More robust error handling for Axios
        if (error.response) {
            console.error("API Error - Status Code:", error.response.status);
            console.error("API Error - Response Data:", error.response.data);
            console.error("API Error - Headers:", error.response.headers);
        } else if (error.request) {
            console.error("Network Error: No response received from server.");
            console.error("Request:", error.request);
        } else {
            console.error("Error setting up request:", error.message);
        }
        console.error("Full Error Object:", error);
    }
}

main();
```

**Java**

```java
//Para Java precisaremos criar um arquivo com o nome de qitech-java-client
//Crie um arquivo chamado pom.xml e cole o final do código dentro dele
// Será necessário dentro do seu projeto criar algumas pastas - crie o seguinte path; src > main > java > com > qitech > api e insira seu arquivo java dentro com o nome de QItechApiClient.java
// Adicione sua private key no diretório raiz de seu projeto no mesmo nível que seu pom
// Para rodar o código abra o terminal ou comand prompt, navegue para a raiz de seu projeto e rode o seguinte código:
// mvn clean install exec:java

//Aqui, importamos as bibliotecas necessárias ao longo do processo de autenticação.
package com.qitech.api;

import com.google.gson.Gson;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.jce.spec.ECPrivateKeySpec;

public class QItechApiClient {

    public static Map<String, String> getAuthHeader(String endpoint, String method, PrivateKey privateKey, String apiKey, Map<String, Object> requestBody) throws NoSuchAlgorithmException {        
        //O objeto de data e hora informado deve estar em UTC e deve seguir o padrão da norma internacional ISO 8601 ("2023-06-26T19:48:32.759844Z")
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'");
        sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
        String timestamp = sdf.format(new Date());

        //Construir hash em MD5 para assinatura no cabeçalho (header) utilizando o payload
        String jsonBody = jsonToString(requestBody);
        String md5Hash = md5Hash(jsonBody);

        //Essas são as infromações necessárias para assinatura do cabeçalho
        Map<String, Object> jwtBody = new HashMap<>();
        jwtBody.put("payload_md5", md5Hash);
        jwtBody.put("timestamp", timestamp);
        jwtBody.put("method", method);
        jwtBody.put("uri", endpoint);

        //Realizar criptografia do header
        JwtBuilder jwtBuilder = Jwts.builder()
                .setClaims(jwtBody)
                .signWith(privateKey, SignatureAlgorithm.ES512);
        String encodedHeaderToken = jwtBuilder.compact();

        //Montar header assinado
        Map<String, String> signedHeader = new HashMap<>();
        signedHeader.put("AUTHORIZATION", encodedHeaderToken);
        signedHeader.put("API-CLIENT-KEY", apiKey);

        return signedHeader;
    }

    public static void main(String[] args) {
        Security.addProvider(new BouncyCastleProvider());
        OkHttpClient client = null;

        try {

            //Utilizaremos as variáveis base_url, endpoint, method e request_body. Neste exemplo faremos um POST no endpoint "/test".
                //As chaves contidas neste exemplo são apenas para fins de demonstração. Por favor, utilize suas próprias chaves.
            final String BASE_URL = "https://api.sandbox.insurance.qitech.app";
            final String PRIVATE_KEY_FILENAME = "private.key";

            final String API_CLIENT_KEY = "SUA API KEY AQUI";
            
            final String METHOD = "GET";  //GET ou POST
            final Map<String, Object> REQUEST_BODY = new HashMap<>();
            REQUEST_BODY.put("name", "QI Tech");

            
            String keyFromFile = readKeyFromFile(PRIVATE_KEY_FILENAME);
            PrivateKey privateKey = getPrivateKey(keyFromFile);

            String endpoint;
            Request request;

            //Para fazer um GET na /test, é necessário inserir a API key ao final, enquanto para fazer um POST no mesmo endpoint, não é necessário

            if ("GET".equalsIgnoreCase(METHOD)) {
                endpoint = "/test/" + API_CLIENT_KEY;

                Map<String, String> headers = getAuthHeader(endpoint, "GET", privateKey, API_CLIENT_KEY, Collections.emptyMap());

                request = new Request.Builder()
                        .url(BASE_URL + endpoint)
                        .headers(okhttp3.Headers.of(headers))
                        .get()
                        .build();

            } else {
                endpoint = "/test";

                Map<String, String> headers = getAuthHeader(endpoint, "POST", privateKey, API_CLIENT_KEY, REQUEST_BODY);

                RequestBody body = RequestBody.create(
                    jsonToString(REQUEST_BODY),
                    MediaType.parse("application/json; charset=utf-8")
                );

                request = new Request.Builder()
                        .url(BASE_URL + endpoint)
                        .headers(okhttp3.Headers.of(headers))
                        .post(body)
                        .build();
            }

            client = new OkHttpClient.Builder()
                    .connectTimeout(30, TimeUnit.SECONDS)
                    .readTimeout(30, TimeUnit.SECONDS)
                    .build();

            System.out.println("--- Sending " + METHOD + " Request ---");
            System.out.println("URL: " + BASE_URL + endpoint);

            try (Response response = client.newCall(request).execute()) {
                System.out.println("\n--- Received Response ---");
                System.out.println("Status Code: " + response.code());
                if (response.body() != null) {
                    System.out.println("Response Body: " + response.body().string());
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (client != null) {
                client.dispatcher().executorService().shutdown();
                client.connectionPool().evictAll();
            }
        }
    }

    // --- Helper Methods ---
    
    private static String readKeyFromFile(String filename) throws IOException {
        String key = new String(Files.readAllBytes(Paths.get(filename)));
        return key.replace("-----BEGIN EC PRIVATE KEY-----", "")
                  .replace("-----END EC PRIVATE KEY-----", "")
                  .replace("-----BEGIN PRIVATE KEY-----", "")
                  .replace("-----END PRIVATE KEY-----", "")
                  .replaceAll("\\s", "");
    }

    private static String jsonToString(Map<String, Object> jsonMap) { return new Gson().toJson(jsonMap); }

    private static String md5Hash(String text) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] array = md.digest(text.getBytes());
        StringBuilder sb = new StringBuilder();
        for (byte b : array) { sb.append(String.format("%02x", b)); }
        return sb.toString();
    }

    private static PrivateKey getPrivateKey(final String encodedPvKey) throws IOException {
        try {
            byte[] derBytes = Base64.getDecoder().decode(encodedPvKey);
            KeyFactory keyFactory = KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME);
            try {
                return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(derBytes));
            } catch (InvalidKeySpecException e) {
                org.bouncycastle.asn1.sec.ECPrivateKey sec1Key = org.bouncycastle.asn1.sec.ECPrivateKey.getInstance(derBytes);
                ECParameterSpec ecParameterSpec = ECNamedCurveTable.getParameterSpec("secp521r1");
                ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(sec1Key.getKey(), ecParameterSpec);
                return keyFactory.generatePrivate(privateKeySpec);
            }
        } catch (Exception e) {
            throw new IOException("Failed to parse private key. Key is corrupted or not a valid EC key.", e);
        }
    }
}

////POM File

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.qitech.api</groupId>
    <artifactId>qitech-api-client</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- HTTP Client -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.12.0</version>
        </dependency>

        <!-- JSON Web Token (JWT) Handling -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.12.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.12.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.12.5</version>
            <scope>runtime</scope>
        </dependency>

        <!-- Cryptography Provider for ES512 -->
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk18on</artifactId>
            <version>1.78</version>
        </dependency>

        <!-- JSON Serialization -->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.10.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>3.2.0</version>
                <configuration>
                    <mainClass>com.qitech.api.QItechApiClient</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

```

**C#**

```c#
//Aqui, importamos as bibliotecas necessárias ao longo do processo de autenticação.
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Jose;
using Newtonsoft.Json;

public static class QiTechAuthGenerator {
    public static string GetAuthorizationHeader(
        string endpoint,
        string method,
        string clientPrivateKey,
        object requestBody)
    {
        string privateKeyBase64 = clientPrivateKey 
            .Replace("-----BEGIN EC PRIVATE KEY-----", "")
            .Replace("-----END EC PRIVATE KEY-----", "")
            .Replace("\n", "")
            .Replace("\r", "");

        using var privateKey = ECDsa.Create();
        privateKey.ImportECPrivateKey(Convert.FromBase64String(privateKeyBase64), out _);

        string payloadToHash;

        if (method.ToUpper() == "GET") {
            payloadToHash = "{}";
        }
        else {
            payloadToHash = JsonConvert.SerializeObject(requestBody);
        }
        
        //Construir hash em MD5 para assinatura no cabeçalho (header) utilizando o payload
        var payloadMd5Hash = CalculateMd5Hash(payloadToHash);

        //Essas são as infromações necessárias para assinatura do cabeçalho
        var jwtBody = new Dictionary<string, object> {
            { "payload_md5", payloadMd5Hash },
            { "timestamp", timestamp },
            { "method", method },
            { "uri", endpoint }
        };

        //Definimos o algoritmo de codificação JWT
        var jwtHeader = new Dictionary<string, object> {
            { "typ", "JWT" },
            { "alg", "ES512" }
        };

        return JWT.Encode(jwtBody, privateKey, JwsAlgorithm.ES512, jwtHeader);
    }

    //Construir hash em MD5 para assinatura no cabeçalho (header) utilizando o payload
    private static string CalculateMd5Hash(string input) {
        using (var md5 = MD5.Create()) {
            byte[] inputBytes = Encoding.UTF8.GetBytes(input);
            byte[] hashBytes = md5.ComputeHash(inputBytes);

            var builder = new StringBuilder();
            foreach (var b in hashBytes) {
                builder.Append(b.ToString("x2"));
            }
            return builder.ToString();
        }
    }
}

public class QiTechApiClient {
    private readonly string _baseUrl;
    private readonly string _apiKey;
    private readonly string _clientPrivateKey;

    public QiTechApiClient(string baseUrl, string apiKey, string clientPrivateKey) {
        _baseUrl = baseUrl;
        _apiKey = apiKey;
        _clientPrivateKey = clientPrivateKey;
    }

    //Realizar criptografia do header
    public async Task<string> CallEndpointAsync(string endpoint, string method, object requestBody) {
        var signedHeader = QiTechAuthGenerator.GetAuthorizationHeader(
            endpoint,
            method,
            _clientPrivateKey,
            requestBody
        );

        var url = $"{_baseUrl}{endpoint}";

        //Montar header assinado
        using (var client = new HttpClient()) {
            client.DefaultRequestHeaders.Add("AUTHORIZATION", signedHeader);
            client.DefaultRequestHeaders.Add("API-CLIENT-KEY", _apiKey);

            HttpResponseMessage httpResponse;

            if (method.ToUpper() == "GET") {
                httpResponse = await client.GetAsync(url);
            }
            else {
                var jsonBody = JsonConvert.SerializeObject(requestBody);
                var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
                httpResponse = await client.PostAsync(url, content);
            }

            httpResponse.EnsureSuccessStatusCode();

            var responseContent = await httpResponse.Content.ReadAsStringAsync();

            return responseContent;
        }
    }
}

public class Program {
    public static async Task Main() {
        string response = "";
        
        //Utilizaremos as variáveis baseUrl, endpoint, method e requestBody. Neste exemplo faremos um POST no endpoint "/test".
        var baseUrl = "https://api.sandbox.insurance.qitech.app";
        var method = "POST";
        var requestBody = new { name = "QI Tech" };
        
        //As chaves contidas neste exemplo são apenas para fins de demonstração. Por favor, utilize suas próprias chaves.
        var apiKey = "SUA API KEY AQUI";
        var clientPrivateKey = @"SUA PRIVATE KEY AQUI";

        try {
            var apiClient = new QiTechApiClient(baseUrl, apiKey, clientPrivateKey);

            //Para fazer um GET na /test, é necessário inserir a API key ao final, enquanto para fazer um POST no mesmo endpoint, não é necessário
            if (method.ToUpper() == "GET") {
                var endpoint = "/test/" + apiKey;
                response = await apiClient.CallEndpointAsync(endpoint, method, null);
            }
            else {
                var endpoint = "/test";
                response = await apiClient.CallEndpointAsync(endpoint, method, requestBody);
            }

            Console.WriteLine("\nAPI Response:");
            Console.WriteLine(response);
        }
        catch (HttpRequestException ex) {
            Console.WriteLine($"\nHTTP Error: {ex.Message}");
        }
        catch (Exception ex) {
            Console.WriteLine($"\nAn unexpected error occurred: {ex.Message}");
        }
    }
}
```

---

# Teste de autenticação

URL: /documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_teste_de_autenticacao_v2

:::info Veja também
- [Exemplo completo de autenticação](./seguros_teste_de_autenticacao_completo)
- [Possíveis erros](./seguros_possiveis_erros)
:::

## 1. Introdução e Configuração Inicial

### Visão Geral
Esta documentação detalha o processo de assinatura e encriptação de cabeçalhos para autenticação segura em requisições à nossa API. O processo garante que as requisições sejam confiáveis e seguras, prevenindo acessos não autorizados e garantindo a integridade dos dados.

### Importar Bibliotecas
Aqui, importamos as bibliotecas necessárias ao longo do processo de autenticação.

**Python**

```python
import json
import requests
from datetime import datetime, timezone
from hashlib import md5
from jose import jwt
```

**PHP**

```php
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Signature\JWSTokenSupport;
use Jose\Component\Signature\Algorithm\ES512;
use Jose\Component\Signature\Serializer\CompactSerializer;
use Jose\Component\Signature\JWSBuilder;
use Jose\Component\KeyManagement\JWKFactory;
```

**Node.js**

```js
const jose = require('jose');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const axios = require('axios');
```

**Java**

```java
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

import java.io.IOException;
import java.io.StringReader;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
```

**C#**

```c#
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using Jose;
using Newtonsoft.Json;
```

### Definir variáveis

Utilizaremos as variáveis _base_url_, _endpoint_, _method_ e _request_body_. Neste exemplo faremos um POST no endpoint "/test".

**Python**

```python
base_url = "https://api.sandbox.insurance.qitech.app"
endpoint = "/test"
method = "POST"
request_body = {"name": "QI Tech"}
```
  

**PHP**

```php
$base_url = "https://api.sandbox.insurance.qitech.app";
$endpoint = "/test";
$method = "POST";
$request_body = ["name" => "QI Tech"];
```
  

**Node.js**

```js
const base_url = 'https://api.sandbox.insurance.qitech.app';
const endpoint = '/test';
const method = 'POST';
const request_body = { name: 'QI Tech' };
```
  

**Java**

```java
private static final String base_url = "https://api.sandbox.insurance.qitech.app";
private static final String endpoint = "/test";
private static final String method = "POST";
private static final Map<String, Object> request_body = new HashMap<>();
static {
    request_body.put("name", "QI Tech");
}
```
  

**C#**

```c#
var base_url = "https://api.sandbox.insurance.qitech.app";
var endpoint = "/test";
var method = "POST";
var request_body = new { name = "QI Tech" };

```
  

## 2. Preparação de Dados para Assinatura

### Inserir dados de criptografia

As chaves contidas neste exemplo são apenas para fins de demonstração. Por favor, utilize suas próprias chaves. 

**Python**

```python
api_key = "f19c6e62-bd82-4334-9839-020810550c44" 

client_private_key = '''-----BEGIN EC PRIVATE KEY-----
MIHbAgEBBEHh1hIeOPE5XNNhn6bxRAmVswsPZ0wZCmzVvP8Tl/LZK9ofVmRVGzll
srU1uezJEyHKYdOHrE2p52xUj+pHzjJvb6AHBgUrgQQAI6GBiQOBhgAEAAofUz1J
hBSOyGHLsnV9Sz0DSWmhl7U+ljqbfa8PKVFWSV3w16I1v2zME5/UzUhHn1gWsjnv
7/ekcLLAQbvqMPNXAfjIhFXLAPzqbB9iCuVua1v0Vgy52rBemOWrJka/Ws2bnKR8
h1N1OxOYeYr6C2jqMygBLktKMAs+282CEiEb4bIv
-----END EC PRIVATE KEY-----'''  
```
  

**PHP**

```php
$api_key = "f19c6e62-bd82-4334-9839-020810550c44"; 

$privateKeyString = "-----BEGIN EC PRIVATE KEY-----
MIHbAgEBBEHh1hIeOPE5XNNhn6bxRAmVswsPZ0wZCmzVvP8Tl/LZK9ofVmRVGzll
srU1uezJEyHKYdOHrE2p52xUj+pHzjJvb6AHBgUrgQQAI6GBiQOBhgAEAAofUz1J
hBSOyGHLsnV9Sz0DSWmhl7U+ljqbfa8PKVFWSV3w16I1v2zME5/UzUhHn1gWsjnv
7/ekcLLAQbvqMPNXAfjIhFXLAPzqbB9iCuVua1v0Vgy52rBemOWrJka/Ws2bnKR8
h1N1OxOYeYr6C2jqMygBLktKMAs+282CEiEb4bIv
-----END EC PRIVATE KEY-----"; 
```
  

**Node.js**

```js
const api_key = 'f19c6e62-bd82-4334-9839-020810550c44'; 

const client_private_key = `-----BEGIN EC PRIVATE KEY-----
MIHbAgEBBEHh1hIeOPE5XNNhn6bxRAmVswsPZ0wZCmzVvP8Tl/LZK9ofVmRVGzll
srU1uezJEyHKYdOHrE2p52xUj+pHzjJvb6AHBgUrgQQAI6GBiQOBhgAEAAofUz1J
hBSOyGHLsnV9Sz0DSWmhl7U+ljqbfa8PKVFWSV3w16I1v2zME5/UzUhHn1gWsjnv
7/ekcLLAQbvqMPNXAfjIhFXLAPzqbB9iCuVua1v0Vgy52rBemOWrJka/Ws2bnKR8
h1N1OxOYeYr6C2jqMygBLktKMAs+282CEiEb4bIv
-----END EC PRIVATE KEY-----`; 
```
  

**Java**

```java
private static final String clientPrivateKey = "MIHbAgEBBEHh1hIeOPE5XNNhn6bxRAmVswsPZ0wZCmzVvP8Tl/LZK9ofVmRVGzll
srU1uezJEyHKYdOHrE2p52xUj+pHzjJvb6AHBgUrgQQAI6GBiQOBhgAEAAofUz1J
hBSOyGHLsnV9Sz0DSWmhl7U+ljqbfa8PKVFWSV3w16I1v2zME5/UzUhHn1gWsjnv
7/ekcLLAQbvqMPNXAfjIhFXLAPzqbB9iCuVua1v0Vgy52rBemOWrJka/Ws2bnKR8
h1N1OxOYeYr6C2jqMygBLktKMAs+282CEiEb4bIv"; 
```
  

**C#**

```c#
var api_key = "f19c6e62-bd82-4334-9839-020810550c44"; 
var client_private_key = @"-----BEGIN EC PRIVATE KEY-----
MIHbAgEBBEHh1hIeOPE5XNNhn6bxRAmVswsPZ0wZCmzVvP8Tl/LZK9ofVmRVGzll
srU1uezJEyHKYdOHrE2p52xUj+pHzjJvb6AHBgUrgQQAI6GBiQOBhgAEAAofUz1J
hBSOyGHLsnV9Sz0DSWmhl7U+ljqbfa8PKVFWSV3w16I1v2zME5/UzUhHn1gWsjnv
7/ekcLLAQbvqMPNXAfjIhFXLAPzqbB9iCuVua1v0Vgy52rBemOWrJka/Ws2bnKR8
h1N1OxOYeYr6C2jqMygBLktKMAs+282CEiEb4bIv
-----END EC PRIVATE KEY-----"; 
```
  

### Formatar data
O objeto de data e hora informado deve estar em UTC e deve seguir o padrão da norma internacional ISO 8601 ("2023-06-26T19:48:32.759844Z")

**Python**

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

**PHP**

```php
$timestamp = gmdate('Y-m-d\TH:i:s.u\Z');
```
  

**Node.js**

```js
const timestamp = new Date().toISOString();
```
  

**Java**

```java
Date now = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'");
String formattedDate = sdf.format(now);
```
  

**C#**

```c#
var timestamp = datetime.now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
```
  

### Definir cabeçalho JWT
Definimos o algoritmo de codificação JWT

**Python**

```python
jwt_header = {
    "typ": "JWT",
    "alg": "ES512"
}
```
  

**PHP**

```php
$header = [
    "typ" => "JWT",
    "alg" => "ES512"
];
```
  

**Node.js**

```js
const jwt_header = {
  typ: 'JWT',
  alg: 'ES512'
};
```
  

**Java**

```java
// Não é necessário
```
  

**C#**

```c#
var jwt_header = new Dictionary<string, object>
{ 
  { "typ", "JWT" },
  { "alg", "ES512" }
};
```
  

### Construir hash em MD5 para assinatura no cabeçalho JSON

Construir hash em MD5 para assinatura no cabeçalho (header) utilizando o payload

**Python**

```python
json_body = json.dumps(request_body)
md5_hash = md5(json_body.encode()).hexdigest()
```
  

**PHP**

```php
$request_body_json = json_encode($request_body);
$md5_hash = md5($request_body_json);
```
  

**Node.js**

```js
const str_body = JSON.stringify(request_body);
const md5_hash = crypto.createHash('md5').update(str_body).digest('hex');
```
  

**Java**

```java
String payloadMd5 = md5Hash(jsonToString(request_body));

...

private static String jsonToString(Map<String, Object> jsonMap) {
    return new com.google.gson.Gson().toJson(jsonMap);
}

...

private static String md5Hash(String text) {
    try {
        java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
        byte[] array = md.digest(text.getBytes());
        StringBuilder sb = new StringBuilder();
        for (byte b : array) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    } catch (java.security.NoSuchAlgorithmException e) {
        return null;
    }
}
```
  

**C#**

```c#
var json_body = JsonConvert.SerializeObject(request_body);
var md5_hash = CalculateMD5Hash(json_body);

.

static string CalculateMD5Hash(string input)
{
    using (MD5 md5 = MD5.Create())
    {
        byte[] inputBytes = Encoding.UTF8.GetBytes(input);
        byte[] hashBytes = md5.ComputeHash(inputBytes);

        StringBuilder builder = new StringBuilder();

        for (int i = 0; i < hashBytes.Length; i++)
        {
            builder.Append(hashBytes[i].ToString("x2"));
        }

        return builder.ToString();
    }
}
```
  

:::caution Atenção!

Requisições dos métodos `GET` e `DELETE` não possuem corpo. O hash md5 dessas requisições deve ser gerado sobre a **string vazia** (`""`), e o valor é sempre a constante abaixo:

```
d41d8cd98f00b204e9800998ecf8427e
```

Esse valor é **diferente** do utilizado no Lending-as-a-Service, que assina `GET` e `DELETE` com o md5 do objeto JSON vazio (`"{}"`), `99914b932bd37a50b983c5e7c90ae93b`. Se você já integra o Lending-as-a-Service, não reaproveite a constante: assinar um `GET` do Insurance-as-a-Service com ela retorna `401` com o código `EGW000006`.
:::

### Os dois erros `401` do header `AUTHORIZATION`

Uma falha no header `AUTHORIZATION` retorna `401` com um de dois códigos, e eles apontam para causas diferentes:

| Status | Código | Descrição |
|---|---|---|
| `401` | `EGW000004` | A assinatura da requisição não pôde ser verificada com a chave pública registrada da integração. |
| `401` | `EGW000006` | O digest do payload assinado não corresponde ao corpo da requisição. |

- `EGW000004` significa que o problema está no **par de chaves**: a assinatura não foi verificada com a chave pública registrada para a sua integração. Confira a [troca de chaves](/documentation/seguros/primeiros_passos/seguros_troca_de_chaves), inclusive o formato do arquivo enviado.
- `EGW000006` significa que a assinatura **foi verificada com sucesso** — a sua chave está correta — e que apenas o `md5` não corresponde ao corpo enviado. Não investigue o par de chaves: confira o hash, começando pela constante de `GET` e `DELETE` acima.

### Construir hash em MD5 para assinatura no cabeçalho Arquivo

Construir hash em MD5 para assinatura no cabeçalho (header) utilizando um arquivo

**Python**

```python
md5_instance = md5()
for chunk in iter(lambda: file.read(4096), b""):
    md5_instance.update(chunk)

file.seek(0)
md5_hash = md5_instance.hexdigest()
```

**PHP**

```php
$md5_instance = md5_file($file);
$md5_hash = hash_file('md5', $file);
```

**Node.js**

```js
const md5_instance = crypto.createHash('md5');
const readStream = fs.createReadStream(file);

readStream.on('data', (chunk) => {
  md5_instance.update(chunk);
});

readStream.on('end', () => {
  const md5_hash = md5_instance.digest('hex');
  file.seek(0);
```

**Java**

```java
MessageDigest md5_instance = MessageDigest.getInstance("MD5");
byte[] buffer = new byte[4096];
int bytesRead;

try (InputStream inputStream = new FileInputStream(file)) {
    while ((bytesRead = inputStream.read(buffer)) != -1) {
        md5_instance.update(buffer, 0, bytesRead);
    }
}

byte[] md5_hashBytes = md5_instance.digest();
StringBuilder md5_hashBuilder = new StringBuilder();

for (byte b : md5_hashBytes) {
    md5_hashBuilder.append(String.format("%02x", b));
}

String md5_hash = md5_hashBuilder.toString();
```

**C#**

```c#
using (var md5_instance = MD5.Create())
{
    using (var stream = File.OpenRead(file))
    {
        byte[] hash = md5_instance.ComputeHash(stream);
        string md5_hash = BitConverter.ToString(hash).Replace("-", "").ToLower();
        stream.Seek(0, SeekOrigin.Begin);
    }
}
```

### Definir o corpo do JWT
Essas são as infromações necessárias para assinatura do cabeçalho

**Python**

```python
jwt_body = {
    "payload_md5": md5_hash,
    "timestamp": timestamp,
    "method": method,
    "uri": endpoint
}
```
  

**PHP**

```php
$payload = [
    "payload_md5" => $md5_hash,
    "timestamp" => $timestamp,
    "method" => $method,
    "uri" => $endpoint
];
```
  

**Node.js**

```js
const jwt_body = {
  payload_md5: md5_hash,
  timestamp: timestamp,
  method: method,
  uri: endpoint
};
```
  

**Java**

```java
Map<String, Object> jwt_body = new HashMap<>();
jwt_body.put("payload_md5", payloadMd5);
jwt_body.put("timestamp", formattedDate);
jwt_body.put("method", method);
jwt_body.put("uri", endpoint);
```
  

**C#**

```c#
// Ajuste: Remover espaços e quebras de linha no meio da chave privada
client_private_key = client_private_key.Replace("-----BEGIN EC PRIVATE KEY-----", "")
                                       .Replace("-----END EC PRIVATE KEY-----", "")
                                       .Replace("\n", "")
                                       .Replace("\r", "");
// Converter a chave privada para ECDsa
using (ECDsa ecdsa = ECDsa.Create())
{
  ecdsa.ImportECPrivateKey(Convert.FromBase64String(client_private_key), out _);
  var jwt_body = new Dictionary<string, object>
    {
      { "payload_md5", md5_hash },
      { "timestamp", timestamp },
      { "method", method },
      { "uri", endpoint }
    };
```
  

### Realizar criptografia do header

**Python**

```python
encoded_header_token = jwt.encode(
    claims=jwt_body,
    key=client_private_key,
    algorithm="ES512",
    headers=jwt_header
)
```
  

**PHP**

```php
$jws = $jwsBuilder
    ->create()
    ->withPayload(json_encode($payload))
    ->addSignature($privateKey, $header)
    ->build();
$serializer = new CompactSerializer();
$jwt = $serializer->serialize($jws, 0);
```
  

**Node.js**

```js
const encoded_header_token = jwt.sign(
  jwt_body,
  client_private_key,
  {
    algorithm: 'ES512',
    header: jwt_header
  }
);
```
  

**Java**

```java
PrivateKey privateKey = getPrivateKey(clientPrivateKey);
JwtBuilder jwtBuilder = Jwts.builder().setClaims(jwt_body).signWith(privateKey, SignatureAlgorithm.ES512);
String encodedHeaderToken = jwtBuilder.compact();

...

public static PrivateKey getPrivateKey(final String encodedPvKey) {
    try {
        final String pvKey = new String(Base64.getDecoder().decode(encodedPvKey));
        PEMParser pemParser = new PEMParser(new StringReader(pvKey));
        PEMKeyPair pemKeyPair = (PEMKeyPair) pemParser.readObject();

        JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
        KeyPair kp = converter.getKeyPair(pemKeyPair);
        pemParser.close();

        return kp.getPrivate();
    } catch (IOException e) {
        throw new RuntimeException("Couldn't load private key");
    }
}
```
  

**C#**

```c#
var encoded_header_token = JWT.Encode(jwt_body, ecdsa, JwsAlgorithm.ES512, jwt_header);
```
  

### Montar header assinado

**Python**

```python
signed_header = {
    "AUTHORIZATION": encoded_header_token,
    "API-CLIENT-KEY": api_key
}
```
  

**PHP**

```php
$headers = [
    'Authorization' => $jwt,
    'API-CLIENT-KEY' => $api_key,
];
```
  

**Node.js**

```js
const signed_header = {
  AUTHORIZATION: encoded_header_token,
  'API-CLIENT-KEY': api_key
};
```
  

**Java**

```java
Map<String, String> headers = new HashMap<>();
headers.put("AUTHORIZATION", encodedHeaderToken);
headers.put("API-CLIENT-KEY", api_key);
```
  

**C#**

```c#
using (var client = new HttpClient())
    client.DefaultRequestHeaders.Clear();
    client.DefaultRequestHeaders.Add("AUTHORIZATION", encoded_header_token);
    client.DefaultRequestHeaders.Add("API-CLIENT-KEY", api_key);
```
  

### Construir a URL da solicitação

**Python**

```python
url = f"{base_url}{endpoint}"
```
  

**PHP**

```php
$url = $base_url . $endpoint;
```
  

**Node.js**

```js
const url = `${base_url}${endpoint}`;
```
  

**Java**

```java
String requestUrl = base_url + endpoint;
```
  

**C#**

```c#
var url = $"{base_url}{endpoint}";
```
  

## 3. Realizar Requisição

**Python**

```python
post_test_response = requests.post(url=url, headers=signed_header, json=request_body)
```
  

**PHP**

```php
$response = \WpOrg\Requests\Requests::post($url, $headers, json_encode($request_body));
```
  

**Node.js**

```js
axios
  .post(url, request_body, { headers: signed_header })
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.error(error);
  });
```
  

**Java**

```java
OkHttpClient client = new OkHttpClient();
MediaType mediaType = MediaType.parse("application/json");
okhttp3.RequestBody requestBody = RequestBody.create(mediaType, jsonToString(request_body));
Request request = new Request.Builder().url(requestUrl).headers(okhttp3.Headers.of(headers))
        .method(method, requestBody).build();
Response response = client.newCall(request).execute();

System.out.println(response.body().string());
```
  

**C#**

```c#
var content = new StringContent(json_body, Encoding.UTF8, "application/json");
var post_test_response = client.PostAsync(url, content).Result;
```

---

# Validação de webhooks

URL: /documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_webhook_v2

:::info Veja também

- [Configurando webhooks](/documentation/seguros/primeiros_passos/seguros_configurando_webhooks)

:::

Cada webhook do Insurance-as-a-Service é assinado com a chave privada da sua integração na QI Tech. Valide a assinatura com a **chave pública da QI Tech** recebida na [configuração dos webhooks](/documentation/seguros/primeiros_passos/seguros_configurando_webhooks) antes de processar qualquer webhook.

## O que é assinado

A assinatura chega no header `AUTHORIZATION` — ou em `QI-AUTHORIZATION`, se você configurou um header próprio chamado `AUTHORIZATION`. Ela é um JWT assinado com **ES512** (curva P-521), cujo payload tem exatamente quatro campos:

| Campo | Descrição | Como validar |
|---|---|---|
| `method` | Método HTTP do webhook. | Deve ser `POST`. |
| `uri` | Caminho da sua URL de webhook, incluindo a query string, se houver. Sem esquema e sem host. | Deve ser igual ao caminho da URL que você configurou, por exemplo `/webhooks/qitech`. |
| `timestamp` | Momento da assinatura, em UTC, no formato `%Y-%m-%dT%H:%M:%S.%fZ`. | Deve estar dentro de uma janela de tolerância — sugerimos 5 minutos. |
| `payload_md5` | MD5 do corpo do webhook **serializado em JSON compacto**. | Deve ser igual ao MD5 que você calcula conforme a seção abaixo. |

## Como calcular o `payload_md5`

O MD5 é calculado sobre o corpo serializado em JSON **compacto**: sem espaços após `,` e `:`, com as chaves na ordem em que chegaram e com os caracteres não-ASCII em UTF-8, sem escape.

O corpo que chega na sua URL tem a mesma informação, mas não os mesmos bytes: ele vem formatado com espaços e com os acentos escapados (`ç`). Por isso:

1. Interprete o corpo recebido como JSON.
2. Serialize-o de novo em JSON compacto, mantendo a ordem das chaves e sem escapar caracteres não-ASCII.
3. Calcule o MD5 em hexadecimal dos bytes UTF-8 dessa serialização.

:::caution Atenção

Calcular o MD5 diretamente sobre os bytes recebidos, ou sobre uma serialização com espaços ou com acentos escapados, nunca confere.

:::

## Exemplo de validação

**Python**

```python
import json
from datetime import datetime, timedelta, timezone
from hashlib import md5

import jwt  # PyJWT com suporte a ES512: pip install "pyjwt[crypto]"

TOLERANCIA = timedelta(minutes=5)

def validar_webhook(headers: dict, corpo_recebido: bytes, chave_publica_qi: str, uri_esperada: str) -> dict:
    """Valida um webhook e devolve o corpo interpretado. Levanta exceção se for inválido.

    `headers` deve ser um dicionário com os nomes em maiúsculas.
    """
    token = headers.get("QI-AUTHORIZATION") or headers["AUTHORIZATION"]
    claims = jwt.decode(token, chave_publica_qi, algorithms=["ES512"])

    corpo = json.loads(corpo_recebido)
    corpo_assinado = json.dumps(corpo, separators=(",", ":"), ensure_ascii=False)
    if claims["payload_md5"] != md5(corpo_assinado.encode("utf-8")).hexdigest():
        raise ValueError("payload_md5 não confere")

    if claims["method"] != "POST":
        raise ValueError("method não confere")

    if claims["uri"] != uri_esperada:
        raise ValueError("uri não confere")

    assinado_em = datetime.strptime(claims["timestamp"], "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.utc)
    if abs(datetime.now(timezone.utc) - assinado_em) > TOLERANCIA:
        raise ValueError("timestamp fora da janela de tolerância")

    return corpo
```

**Node.js**

```js
const crypto = require('node:crypto');

const TOLERANCIA_MS = 5 * 60 * 1000;

// Valida um webhook e devolve o corpo interpretado. Lança erro se for inválido.
// `headers` com os nomes em minúsculas, como no Node.js; `corpoRecebido` é o corpo bruto (string).
function validarWebhook(headers, corpoRecebido, chavePublicaQi, uriEsperada) {
  const token = headers['qi-authorization'] || headers['authorization'];
  const [cabecalho, payload, assinatura] = token.split('.');

  const { alg } = JSON.parse(Buffer.from(cabecalho, 'base64url').toString('utf8'));
  if (alg !== 'ES512') {
    throw new Error('algoritmo inesperado');
  }

  const assinaturaValida = crypto.verify(
    'sha512',
    Buffer.from(`${cabecalho}.${payload}`),
    { key: chavePublicaQi, dsaEncoding: 'ieee-p1363' },
    Buffer.from(assinatura, 'base64url'),
  );
  if (!assinaturaValida) {
    throw new Error('assinatura inválida');
  }

  const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));

  const corpo = JSON.parse(corpoRecebido);
  const corpoAssinado = JSON.stringify(corpo);
  const md5 = crypto.createHash('md5').update(corpoAssinado, 'utf8').digest('hex');
  if (claims.payload_md5 !== md5) {
    throw new Error('payload_md5 não confere');
  }

  if (claims.method !== 'POST') {
    throw new Error('method não confere');
  }

  if (claims.uri !== uriEsperada) {
    throw new Error('uri não confere');
  }

  const assinadoEm = Date.parse(claims.timestamp);
  if (Math.abs(Date.now() - assinadoEm) > TOLERANCIA_MS) {
    throw new Error('timestamp fora da janela de tolerância');
  }

  return corpo;
}
```

## Dados para testar

Use os dados abaixo para conferir a sua implementação. Eles foram assinados com uma chave de exemplo, para a URL `https://seu-dominio.com.br/webhooks/qitech` — a `uri_esperada` é `/webhooks/qitech`.

:::info Informação

Este exemplo foi assinado em 05/10/2026. Para testá-lo, desative a verificação do `timestamp`.

:::

```text title="Chave pública de exemplo"
-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA9TmX4OOkJUMWJSKrL+4znIYW/phK
0w1KnKI+F9pJiI5cjG56sfA0lZnQZ5pa3W5SDWTSUXPdwuOQECeA10Ly2fsBJ8Mo
zCiJ75Tx5/NlaZZEef8gfTHooHfNqJbf4nxerL6yUnXndZa7PfZl/GNDVbFqWK6t
yLOHXS2L7tx+UDEO90Q=
-----END PUBLIC KEY-----
```

```text title="Header AUTHORIZATION"
eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkX21kNSI6IjgxMzVhZjAxMDczNDNiYzJkYjBhODIxM2U1YTZjNzU0IiwidGltZXN0YW1wIjoiMjAyNi0xMC0wNVQxNTo1OToyMS42OTc3MThaIiwibWV0aG9kIjoiUE9TVCIsInVyaSI6Ii93ZWJob29rcy9xaXRlY2gifQ.AVNn4ASJQqHMBO2Q7rLrO-umRFCae9x8Elc296rXzpNRFJ4f3dHjcOM2rVnOj7h8b68NGYCh7ztCt5f7W0s04kB-AaHqm2cZqeGvdKc4ha7qM7YqhqjFYex7MnF7BCLL77BsOyOM9Hx4AiXRe2zS_dD2ReR3bHBFmc-pqT1mnjypsnE7
```

```json title="Corpo como recebido"
{"status": "cancelled", "webhook_type": "insurance.order.status_changed", "key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90", "event_datetime": "2026-10-05T15:59:20Z", "data": {"order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90", "requester_key": "3f1c9a2e-4b7d-4e0a-9c55-2d8e7f6a1b30", "status": "cancelled", "customer_document_number": "96969879003", "reason": "Cliente desistiu da contratação", "product_count": 1, "sequence": 2, "schema_version": 1}}
```

A serialização compacta desse corpo tem o MD5 `8135af0107343bc2db0a8213e5a6c754`, igual ao `payload_md5` do token.