# QI Tech — Insurance-as-a-Service

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

Índice:
- Cancelar apólice (/documentation/seguros/apolices/cancelar_apolice)
- Consultar apólice (/documentation/seguros/apolices/consultar_apolice)
- Início (/documentation/seguros/apolices/inicio)
- Listar apólices de um pedido (/documentation/seguros/apolices/listar_apolices)
- Webhooks da Apólice (/documentation/seguros/apolices/webhooks)
- Consultar produto (/documentation/seguros/catalogo/consultar_produto)
- Início (/documentation/seguros/catalogo/inicio)
- Listar produtos (/documentation/seguros/catalogo/listar_produtos)
- Criar cotação (/documentation/seguros/cotacao/criar_cotacao)
- Início (/documentation/seguros/cotacao/inicio)
- Simular faixa de preço (/documentation/seguros/cotacao/simular_precos)
- Consultar extrato (/documentation/seguros/financeiro/consultar_extrato)
- Consultar saldo (/documentation/seguros/financeiro/consultar_saldo)
- Início (/documentation/seguros/financeiro/inicio)
- Listar repasses (/documentation/seguros/financeiro/listar_transferencias)
- Autenticação (/documentation/seguros/introducao/autenticacao)
- Introdução (/documentation/seguros/introducao/inicio)
- Cancelar pedido (/documentation/seguros/pedidos/cancelar_pedido)
- Consultar pedido (/documentation/seguros/pedidos/consultar_pedido)
- Criar pedido (/documentation/seguros/pedidos/criar_pedido)
- Início (/documentation/seguros/pedidos/inicio)
- Listar pedidos (/documentation/seguros/pedidos/listar_pedidos)
- Webhooks do Pedido (/documentation/seguros/pedidos/webhooks)
- 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)

---

# Cancelar apólice

URL: /documentation/seguros/apolices/cancelar_apolice

Cancela uma **única** apólice; as demais apólices do mesmo pedido não são afetadas. O cancelamento é sempre **assíncrono e em duas fases**: a solicitação coloca a apólice em `cancellation_requested` e o cancelamento efetivo (`canceled`) só ocorre quando a seguradora confirma — até lá, a cobertura permanece em vigor.

Para cancelar **todas** as apólices de um pedido de uma vez, use o [cancelamento do pedido](/documentation/seguros/pedidos/cancelar_pedido) em um pedido emitido.

## Devolução de prêmio

Na confirmação do cancelamento, a QI Tech determina a base e calcula o valor da devolução automaticamente, sobre o prêmio bruto (`gross_premium_amount`):

| Momento do cancelamento | Base de devolução |
|---|---|
| Até 7 dias corridos da confirmação da emissão (`issued`) — direito de arrependimento | Devolução **integral** do prêmio. |
| Após 7 dias | Devolução **proporcional** ao período de cobertura não decorrido (pró-rata). |

A confirmação do cancelamento chega pelo [webhook de cancelamento](/documentation/seguros/apolices/webhooks#apolice-cancelada) e a devolução ao pagador é executada pela QI Tech.

## Request

ENDPOINT /v1/insurance/policies/{policy_key}/cancel
MÉTODO POST

### Path params

| Parâmetro | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `policy_key` | string | obrigatório | Chave da apólice. |

```json title="Request Body"
{
  "reason": "Cliente solicitou o cancelamento"
}
```

| Campo | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `reason` | string | opcional | Motivo do cancelamento, registrado na trilha de eventos e ecoado no webhook de cancelamento. |

## Response

STATUS 202

```json title="Response Body"
{
  "policy_key": "0b6e7c1a-9a4e-4c1e-b1d4-2f5a8c9e0d31",
  "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
  "status": "cancellation_requested"
}
```

A conclusão do cancelamento chega pelo [webhook de apólice](/documentation/seguros/apolices/webhooks) com status `canceled`, e pode ser acompanhada pela [consulta de apólice](/documentation/seguros/apolices/consultar_apolice).

### Semântica por status

| Status atual da apólice | Resultado |
|---|---|
| `issued` | `202` — cancelamento solicitado à seguradora. |
| `cancellation_requested` | `202` — idempotente: não gera segunda solicitação e retorna o mesmo corpo. |
| `issuance_requested` | `409` — a apólice ainda não está em vigor. Para desfazer a venda inteira nesse estágio, cancele o **pedido** (a solicitação fica retida e cancela a apólice automaticamente assim que a emissão confirmar); para cancelar só esta apólice, aguarde a emissão confirmar. |
| `canceled` | `409` — já cancelada. |
| `finished` | `409` — a vigência já terminou. |
| outra integração / inexistente | `404` |

## Possíveis erros

Todo erro (`non-2xx`) retorna o corpo padrão `{ "title", "description", "translation", "code" }` — trate programaticamente **apenas** o campo `code`.

| Status | Código | Descrição |
|---|---|---|
| `401` / `403` | — | Falha de autenticação ou autorização. |
| `404` | `POL000001` | Apólice inexistente ou pertencente a outra integração. |
| `409` | `POL000010` | A apólice já foi cancelada. |
| `409` | `POL000011` | A vigência da cobertura já terminou. |
| `409` | `POL000012` | A apólice ainda não está em vigor. |
| `429` | — | Limite de requisições excedido — repita com backoff. |
| `500` | `QIT000500` | Erro interno — a solicitação é idempotente, repita a chamada. |
| `503` | `POL000030` | Serviço indisponível — a solicitação é idempotente, repita a chamada. |

---

# Consultar apólice

URL: /documentation/seguros/apolices/consultar_apolice

Retorna o detalhe de uma apólice: identidade e chaves de correlação, status, número da apólice na seguradora, a classificação do produto, a decomposição do prêmio (bruto, IOF e líquido), a vigência, as coberturas efetivas e a trilha de eventos.

## Request

ENDPOINT /v1/insurance/policies/{policy_key}
MÉTODO GET

### Path params

| Parâmetro | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `policy_key` | string | obrigatório | Chave da apólice, obtida na [listagem por pedido](/documentation/seguros/apolices/listar_apolices) ou nos [webhooks de apólice](/documentation/seguros/apolices/webhooks). |

## Response

STATUS 200

```json title="Response Body"
{
  "policy_key": "0b6e7c1a-9a4e-4c1e-b1d4-2f5a8c9e0d31",
  "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
  "provider_product_key": "9d1f8c7a-3b21-4e60-8a2f-1c5d7e9b0a44",
  "provider_key": "c4a2e8b0-1f6d-4e3a-9c7b-5d0a2e8f4b61",
  "status": "issued",
  "external_policy_number": "APL-2026-000123",
  "customer_document_number": "96969879003",
  "product_category": "insurance",
  "insurance_class": {
    "name": "credit_life",
    "class_number": "0977",
    "group_number": "09"
  },
  "regulator_registration": "15414.900388/2015-21",
  "gross_premium_amount": 617.28,
  "iof_amount": 2.35,
  "net_premium_amount": 614.93,
  "term": {
    "start_date": "2026-07-16",
    "end_date": "2027-07-15"
  },
  "effective_services": [
    {
      "effective_service_key": "7f3a9c2e-0b5d-4e8a-a1c6-9d4b2e7f0a53",
      "provider_service_key": "0a3c5e7f-2b4d-4a6c-8e0f-1a3b5c7d9e2f",
      "service_type": {
        "code": "credit_life",
        "name": "Prestamista (Credit Life)"
      },
      "service_category": "insurance",
      "regulator_registration": null,
      "insured_amount": 150000.00,
      "gross_premium_amount": 617.28,
      "deductible_data": {
        "deductible_type": "monetary_amount",
        "value": 1500.00
      },
      "waiting_period_days": 30,
      "service_attributes": {},
      "term": {
        "start_date": "2026-07-16",
        "end_date": "2027-07-15"
      }
    }
  ],
  "events": [
    {
      "new_status": "issuance_requested",
      "agent_type": "system",
      "created_at": "2026-07-16T14:03:25.481Z"
    },
    {
      "new_status": "issued",
      "agent_type": "provider",
      "created_at": "2026-07-16T15:00:00.000Z"
    }
  ]
}
```

### Atributos da resposta

| Campo | Tipo | Descrição |
|---|---|---|
| `policy_key` | string | Chave única da apólice. |
| `order_key` | string | Chave do pedido que originou a apólice. |
| `provider_product_key` | string | Chave do produto no catálogo. |
| `provider_key` | string | Chave da seguradora emissora. |
| `status` | string | Status atual. Veja o [ciclo de vida](/documentation/seguros/apolices/inicio). |
| `external_policy_number` | string | Número da apólice na seguradora. `null` até a emissão ser confirmada (`issued`). |
| `customer_document_number` | string | Documento do segurado. |
| `product_category` | string | Categoria do produto: `insurance`, `capitalization` ou `benefit`. |
| `insurance_class` | object | Ramo do seguro: `{ name, class_number, group_number }`. `null` para produtos não securitários. `class_number` e `group_number` são strings — os zeros à esquerda são significativos. |
| `regulator_registration` | string | Registro do produto no regulador (ex.: processo SUSEP). |
| `gross_premium_amount` | number | Prêmio bruto (o valor pago pelo segurado), com IOF. |
| `iof_amount` | number | Componente de IOF do prêmio. |
| `net_premium_amount` | number | Prêmio líquido (bruto menos IOF). |
| `term` | object | Vigência da apólice: `{ start_date, end_date }`. |
| `effective_services` | array | Coberturas efetivas da apólice. |
| `events` | array | Trilha de transições de status, em ordem cronológica — é dela que se derivam os instantes do ciclo de vida (emissão, solicitação de cancelamento, cancelamento). |

#### Objeto em `effective_services`

| Campo | Tipo | Descrição |
|---|---|---|
| `effective_service_key` | string | Chave única da cobertura efetiva. |
| `provider_service_key` | string | Chave da cobertura no catálogo. |
| `service_type` | object | Tipo da cobertura: `{ code, name }`. |
| `service_category` | string | Categoria da cobertura: `insurance`, `capitalization` ou `benefit`. |
| `regulator_registration` | string | Registro próprio da cobertura no regulador. `null` quando herda o do produto. |
| `insured_amount` | number | Importância segurada contratada. |
| `gross_premium_amount` | number | Prêmio bruto da cobertura. |
| `deductible_data` | object | Franquia contratada: `{ deductible_type, value }`. |
| `waiting_period_days` | integer | Carência em dias. |
| `service_attributes` | object | Atributos fixos da cobertura. |
| `term` | object | Vigência da cobertura: `{ start_date, end_date }`. |

#### Objeto em `events`

| Campo | Tipo | Descrição |
|---|---|---|
| `new_status` | string | O status assumido na transição. |
| `agent_type` | string | Quem causou a transição: `requester` (sua integração), `provider` (seguradora), `system` (automático) ou `operator` (operação QI Tech). |
| `created_at` | string | Instante da transição. |

## Possíveis erros

Todo erro (`non-2xx`) retorna o corpo padrão `{ "title", "description", "translation", "code" }` — trate programaticamente **apenas** o campo `code`.

| Status | Código | Descrição |
|---|---|---|
| `401` / `403` | — | Falha de autenticação ou autorização. |
| `404` | `POL000001` | Apólice inexistente ou pertencente a outra integração — os casos são indistinguíveis. |
| `429` | — | Limite de requisições excedido — repita com backoff. |
| `500` | `QIT000500` | Erro interno — seguro repetir a chamada. |
| `503` | `POL000030` | Serviço indisponível — seguro repetir a chamada. |

---

# Início

URL: /documentation/seguros/apolices/inicio

A **apólice** (`policy`) é o contrato de seguro individual — uma por produto vendido em um pedido emitido. Ela nasce na emissão do pedido, é registrada junto à seguradora e passa a ser a fonte da verdade sobre a cobertura: número de apólice na seguradora, vigência, coberturas efetivas, prêmio decomposto e trilha de eventos.

## Ciclo de vida da apólice

![Fluxo de status da apólice, da solicitação de emissão ao encerramento](/img/diagrams/seguros-apolices-inicio.svg)

_Como ler o diagrama: **contorno tracejado** = status transitório que não gera webhook · **azul** = emissão confirmada (gera webhook) · **verde** = encerramento natural (não gera webhook) · **vermelho** = cancelada (gera webhook)._

| Status | Significado |
|---|---|
| `issuance_requested` | Apólice criada na emissão do pedido; a emissão foi solicitada à seguradora e aguarda confirmação. |
| `issued` | A seguradora confirmou a emissão. O `external_policy_number` (número da apólice na seguradora) passa a estar disponível. A cobertura está em vigor. |
| `cancellation_requested` | Um cancelamento foi solicitado e aguarda a confirmação da seguradora. **A cobertura permanece em vigor** enquanto o cancelamento não é confirmado. |
| `canceled` | A seguradora confirmou o cancelamento. A devolução de prêmio, quando devida, é calculada e processada automaticamente. |
| `finished` | A vigência da apólice chegou ao fim naturalmente (`term.end_date`). Status terminal, sem evento — o fim de vigência é conhecido desde a emissão. |

## Devolução de prêmio no cancelamento

Quando um cancelamento é confirmado, a QI Tech determina a base de devolução e calcula o valor automaticamente, sobre o prêmio bruto (`gross_premium_amount`):

- **Dentro de 7 dias corridos** da confirmação da emissão (`issued`) — direito de arrependimento: devolução **integral** do prêmio.
- **Após 7 dias**: devolução **proporcional** ao período de cobertura não decorrido (pró-rata).

A devolução é calculada na confirmação do cancelamento e executada ao pagador pela própria QI Tech.

## Endpoints

| Endpoint | Descrição |
|---|---|
| [`GET /v1/insurance/policies/{policy_key}`](/documentation/seguros/apolices/consultar_apolice) | Detalha uma apólice. |
| [`GET /v1/insurance/policies?order_key=`](/documentation/seguros/apolices/listar_apolices) | Lista as apólices de um pedido emitido. |
| [`POST /v1/insurance/policies/{policy_key}/cancel`](/documentation/seguros/apolices/cancelar_apolice) | Cancela uma apólice individualmente. |

---

# Listar apólices de um pedido

URL: /documentation/seguros/apolices/listar_apolices

Lista as apólices geradas por um pedido emitido — **a** forma de descobrir as `policy_key` de um pedido, já que a consulta de pedido não retorna apólices. Retorna resumos; para o detalhe completo, use a [consulta de apólice](/documentation/seguros/apolices/consultar_apolice).

## Request

ENDPOINT /v1/insurance/policies
MÉTODO GET

### Query params

| Parâmetro | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `order_key` | string | obrigatório | Chave do pedido cujas apólices serão listadas. |

```python title="Exemplo de chamada"
GET /v1/insurance/policies?order_key=5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90
```

## Response

STATUS 200

```json title="Response Body"
{
  "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
  "policies": [
    {
      "policy_key": "0b6e7c1a-9a4e-4c1e-b1d4-2f5a8c9e0d31",
      "provider_product_key": "9d1f8c7a-3b21-4e60-8a2f-1c5d7e9b0a44",
      "provider_key": "c4a2e8b0-1f6d-4e3a-9c7b-5d0a2e8f4b61",
      "status": "issued",
      "external_policy_number": "APL-2026-000123",
      "gross_premium_amount": 617.28,
      "term": {
        "start_date": "2026-07-16",
        "end_date": "2027-07-15"
      },
      "created_at": "2026-07-16T14:03:25.481Z"
    }
  ]
}
```

### Atributos da resposta

| Campo | Tipo | Descrição |
|---|---|---|
| `order_key` | string | Chave do pedido consultado. |
| `policies` | array | Resumo de cada apólice do pedido. Os campos são um subconjunto da [consulta de apólice](/documentation/seguros/apolices/consultar_apolice). |

:::info Consistência eventual após a emissão
As apólices são criadas de forma **assíncrona** logo após o webhook de `emitted` do pedido. Imediatamente após a emissão, esta lista pode vir vazia ou menor que o `product_count` do webhook enquanto as apólices são materializadas. Aguarde os [webhooks de apólice](/documentation/seguros/apolices/webhooks) ou consulte novamente em instantes.
:::

Um `order_key` desconhecido, ainda não processado ou pertencente a outra integração retorna `200` com a lista vazia — os casos são indistinguíveis.

## Possíveis erros

Todo erro (`non-2xx`) retorna o corpo padrão `{ "title", "description", "translation", "code" }` — trate programaticamente **apenas** o campo `code`.

| Status | Código | Descrição |
|---|---|---|
| `400` | `QIT000001` | Parâmetro `order_key` ausente. |
| `401` / `403` | — | Falha de autenticação ou autorização. |
| `429` | — | Limite de requisições excedido — repita com backoff. |
| `500` | `QIT000500` | Erro interno — seguro repetir a chamada. |
| `503` | `POL000030` | Serviço indisponível — seguro repetir a chamada. |

---

# Webhooks da Apólice

URL: /documentation/seguros/apolices/webhooks

Os marcos do ciclo de vida da apólice são notificados por webhooks do tipo `insurance.policy.status_changed`. Dois status geram evento: `issued` (emissão confirmada pela seguradora) e `canceled` (cancelamento confirmado). Os status transitórios (`issuance_requested`, `cancellation_requested`) e o encerramento natural de vigência (`finished`) **não** geram webhook — os transitórios são resultado das suas próprias chamadas, e o fim de vigência é conhecido desde a emissão pelo `term.end_date`.

:::info Configuração de webhooks
Para receber webhooks é necessário ter uma URL de callback configurada. Veja [Autenticação — Recebimento de webhooks](/documentation/seguros/introducao/autenticacao#recebimento-de-webhooks).
:::

## Estrutura do webhook

| Campo | Tipo | Descrição |
|---|---|---|
| `webhook_type` | string | Sempre `insurance.policy.status_changed`. |
| `webhook_datetime` | string | Data e hora do evento no formato ISO 8601. |
| `data` | object | Dados do evento. Veja tabela abaixo. |

#### Atributos de `data`

| Campo | Tipo | Descrição |
|---|---|---|
| `policy_key` | string | Chave da apólice. |
| `order_key` | string | Chave do pedido que originou a apólice — a correlação com a sua venda. |
| `status` | string | Novo status: `issued` ou `canceled`. |
| `external_policy_number` | string | Número da apólice na seguradora. Presente no evento de `issued`. |
| `reason` | string | Motivo do cancelamento. Presente no evento de `canceled`, quando informado. |

```json title="Estrutura padrão do webhook"
{
  "webhook_type": "insurance.policy.status_changed",
  "webhook_datetime": "2026-07-16T15:00:00Z",
  "data": {
    "policy_key": "0b6e7c1a-9a4e-4c1e-b1d4-2f5a8c9e0d31",
    "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
    "status": "status",
    "external_policy_number": "APL-2026-000123",
    "reason": null
  }
}
```

## Eventos por status

### Apólice emitida

STATUS issued

Enviado quando a seguradora confirma a emissão. A partir deste evento o `external_policy_number` está disponível e a cobertura está formalmente em vigor. Como cada apólice é emitida de forma independente, um pedido com N produtos gera N eventos deste tipo — possivelmente em momentos diferentes.

```json title="Webhook Body"
{
  "webhook_type": "insurance.policy.status_changed",
  "webhook_datetime": "2026-07-16T15:00:00Z",
  "data": {
    "policy_key": "0b6e7c1a-9a4e-4c1e-b1d4-2f5a8c9e0d31",
    "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
    "status": "issued",
    "external_policy_number": "APL-2026-000123",
    "reason": null
  }
}
```

---

### Apólice cancelada

STATUS canceled

Enviado quando a seguradora confirma o cancelamento — solicitado pelo [cancelamento de apólice](/documentation/seguros/apolices/cancelar_apolice) ou pelo [cancelamento pós-emissão do pedido](/documentation/seguros/pedidos/cancelar_pedido). A devolução de prêmio devida (integral dentro do direito de arrependimento, pró-rata depois) é calculada neste momento e executada pela QI Tech.

```json title="Webhook Body"
{
  "webhook_type": "insurance.policy.status_changed",
  "webhook_datetime": "2026-08-02T09:30:00Z",
  "data": {
    "policy_key": "0b6e7c1a-9a4e-4c1e-b1d4-2f5a8c9e0d31",
    "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
    "status": "canceled",
    "external_policy_number": "APL-2026-000123",
    "reason": "Cliente solicitou o cancelamento"
  }
}
```

:::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.
:::

---

# Consultar produto

URL: /documentation/seguros/catalogo/consultar_produto

Retorna o **envelope de venda** de um produto habilitado para a sua integração: as coberturas ativas com seus espaços de opções, as dependências entre coberturas, os seus valores padrão e a sua faixa de comissão. Com essa resposta você tem tudo o que precisa para montar uma [cotação](/documentation/seguros/cotacao/criar_cotacao) válida.

## Request

ENDPOINT /v1/product_catalog/products/{product_key}
MÉTODO GET

### Path params

| Parâmetro | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `product_key` | string | obrigatório | Chave do produto, obtida na [listagem de produtos](/documentation/seguros/catalogo/listar_produtos). |

## Response

STATUS 200

```json title="Response Body"
{
  "product_key": "9d1f8c7a-3b21-4e60-8a2f-1c5d7e9b0a44",
  "name": "Prestamista Master",
  "product_category": "insurance",
  "insurance_class": "credit_life",
  "contract_instrument_type": "ticket",
  "regulator_registration": "15414.900123/2025-77",
  "provider_name": "QI Seguradora",
  "commission_bounds": {
    "minimum_rate": 0.05,
    "maximum_rate": 0.20,
    "default_rate": 0.15
  },
  "services": [
    {
      "service_key": "0a3c5e7f-2b4d-4a6c-8e0f-1a3b5c7d9e2f",
      "service_type": "credit_life",
      "service_category": "insurance",
      "mandatory": true,
      "maximum_insured_amount": 500000.00,
      "deductible_options": {
        "deductible_type": "monetary_amount",
        "options_type": "list",
        "options": [0.00, 1500.00, 3000.00]
      },
      "waiting_period_options": {
        "waiting_period_type": "days",
        "options_type": "range",
        "options": {
          "minimum": 0,
          "maximum": 90,
          "step": 30
        }
      },
      "indemnity_unit_options": null,
      "regulator_registration": null,
      "dependencies": {
        "include": [],
        "exclude": []
      },
      "service_attributes": null,
      "default_configuration": {
        "insured_amount_basis": "percentage_of_risk_value",
        "insured_amount": null,
        "insured_amount_percentage": 0.8000,
        "unit_amount": null,
        "unit_count": null,
        "deductible_data": {
          "deductible_type": "monetary_amount",
          "value": 1500.00
        },
        "waiting_period_days": 30
      }
    }
  ]
}
```

### Atributos da resposta

| Campo | Tipo | Descrição |
|---|---|---|
| `product_key` | string | Chave única do produto. |
| `name` | string | Nome comercial do produto. |
| `product_category` | string | Categoria do produto: `insurance`, `capitalization` ou `benefit`. |
| `insurance_class` | string | Ramo SUSEP do produto. `null` para produtos não-seguro. |
| `contract_instrument_type` | string | Instrumento contratual do produto: `ticket` (bilhete) ou `policy` (apólice). Determina o fluxo de contratação e quais métodos de aceite são válidos — veja [Criar pedido](/documentation/seguros/pedidos/criar_pedido). `null` para produtos não securitários. |
| `regulator_registration` | string | Registro do produto na SUSEP (Código SUSEP). |
| `provider_name` | string | Nome da seguradora parceira. |
| `commission_bounds` | object | A **sua** faixa de comissão para este produto: `minimum_rate`, `maximum_rate` e `default_rate` (taxas decimais com 4 casas). A comissão da cotação/pedido é validada contra `[minimum_rate, maximum_rate]`; se omitida, vale `default_rate`. |
| `services` | array | Coberturas **ativas** do produto. |

#### Objeto em `services`

| Campo | Tipo | Descrição |
|---|---|---|
| `service_key` | string | Chave única da cobertura. Use-a na lista `services` da cotação e do pedido. |
| `service_type` | string | Tipo da cobertura (ex.: `credit_life`). |
| `service_category` | string | Categoria da cobertura: `insurance`, `capitalization` ou `benefit`. |
| `mandatory` | boolean | Cobertura obrigatória em toda venda do produto. Uma seleção que a omita é rejeitada com `MISSING_MANDATORY_SERVICE`. |
| `maximum_insured_amount` | number | Importância segurada máxima aceita. `null` para coberturas sem importância segurada (benefícios). |
| `deductible_options` | object | Envelope tipado das franquias aceitas (veja abaixo). `null` indica que a cobertura **não tem franquia** — enviar `deductible_data` para ela é rejeitado. |
| `waiting_period_options` | object | Envelope tipado das carências aceitas (veja abaixo). `null` indica que a cobertura **não tem carência**. |
| `indemnity_unit_options` | object | Envelope tipado das unidades de indenização aceitas, para coberturas precificadas por unidade (ex.: "R$ 100 por diária, até 60 diárias"). `null` indica que a cobertura **não é precificada por unidade** — enviar `unit_amount`/`unit_count` para ela é rejeitado com `OUT_OF_OPTION_SPACE`. |
| `regulator_registration` | string | Registro SUSEP próprio da cobertura. `null` indica que a cobertura herda o registro do produto. |
| `dependencies` | object | Regras estruturais entre coberturas deste produto: `include` (chaves de coberturas pré-requisito — toda cobertura listada precisa estar na seleção) e `exclude` (chaves mutuamente exclusivas — não podem coexistir na seleção). |
| `service_attributes` | object | Atributos fixos da cobertura definidos pela seguradora (ex.: quantidades de um benefício). Ecoados na cotação. |
| `default_configuration` | object | O **seu** padrão configurado para esta cobertura — o que a cotação preenche automaticamente quando a seleção omite `services`. Todos os campos de valor são emitidos, com `null` nos que não se aplicam; `insured_amount_basis` diz qual está ativo. Os valores vêm na **mesma forma** que você enviaria na seleção, prontos para reuso. `null` quando não há padrão configurado. |

#### Envelopes de opções (`deductible_options` / `waiting_period_options` / `indemnity_unit_options`)

| Campo | Tipo | Descrição |
|---|---|---|
| `deductible_type` | string | Tipo da franquia: `monetary_amount`, `days` ou `percentage_of_insured_amount`. Presente em `deductible_options`. |
| `waiting_period_type` | string | Tipo da carência: `days`. Presente em `waiting_period_options`. |
| `indemnity_unit_type` | string | Tipo da unidade de indenização (ex.: `daily`). Presente em `indemnity_unit_options`. |
| `options_type` | string | Forma do espaço de opções: `list` (lista de valores aceitos) ou `range` (intervalo `{minimum, maximum, step}`; `step` `null` indica intervalo contínuo). |
| `options` | array / object | Os valores aceitos, na forma indicada por `options_type`. |

#### Objeto `default_configuration`

| Campo | Tipo | Descrição |
|---|---|---|
| `insured_amount_basis` | string | Base da importância segurada: `monetary_amount` (valor absoluto), `percentage_of_risk_value` (percentual do valor do objeto de risco) ou `unit_amount_times_count` (valor por unidade × quantidade de unidades). Diz qual dos campos de valor abaixo está ativo. |
| `insured_amount` | number | Importância segurada em reais. Presente quando a base é `monetary_amount`; `null` nas demais. |
| `insured_amount_percentage` | number | Percentual do valor do objeto de risco, em `(0, 1]` (`1.0000` = 100%). Presente quando a base é `percentage_of_risk_value`; `null` nas demais. |
| `unit_amount` | number | Valor por unidade de indenização. Presente quando a base é `unit_amount_times_count`; `null` nas demais. |
| `unit_count` | integer | Quantidade de unidades de indenização. Presente quando a base é `unit_amount_times_count`; `null` nas demais. |
| `deductible_data` | object | Franquia padrão, na mesma forma tipada enviada na seleção: `{ deductible_type, value }`. |
| `waiting_period_days` | integer | Carência padrão, em dias. |

## Possíveis erros

Todo erro (`non-2xx`) retorna o corpo padrão `{ "title", "description", "translation", "code" }` — trate programaticamente **apenas** o campo `code`.

| Status | Código | Descrição |
|---|---|---|
| `401` / `403` | — | Falha de autenticação ou autorização. |
| `404` | `CAT000040` | O produto não existe, está inativo ou não está habilitado para a sua integração — os três casos são indistinguíveis. |
| `429` | — | Limite de requisições excedido — repita com backoff. |
| `500` | `QIT000500` | Erro interno — seguro repetir a chamada. |
| `503` | `CAT000033` | Serviço indisponível — seguro repetir a chamada. |

---

# Início

URL: /documentation/seguros/catalogo/inicio

O **Catálogo de Produtos** é a superfície de descoberta do Insurance-as-a-Service: ele responde "o que a minha integração pode vender?" e "como monto uma seleção válida para cotar?". São dois endpoints somente de leitura, escopados aos produtos habilitados para a sua integração.

## Conceitos

| Conceito | Descrição |
|---|---|
| **Produto** (`product`) | Um produto de seguro de uma seguradora parceira (ex.: um prestamista, um seguro de vida em grupo). É a unidade de venda: cada produto vendido em um pedido gera uma apólice. Endereçado por `product_key`. |
| **Cobertura** (`service`) | Uma cobertura ou benefício que compõe o produto (ex.: morte, invalidez por acidente, assistência funeral). Pode ser obrigatória (`mandatory`) ou opcional. Endereçada por `service_key`. |
| **Espaço de opções** | Os limites configuráveis de cada cobertura: importância segurada máxima (`maximum_insured_amount`) e os envelopes tipados de franquia (`deductible_options`) e carência (`waiting_period_options`) aceitos. Uma seleção fora do espaço de opções é rejeitada na cotação. |
| **Dependências** | Regras estruturais entre coberturas do mesmo produto: `include` (coberturas pré-requisito) e `exclude` (coberturas mutuamente exclusivas). |
| **Faixa de comissão** (`commission_bounds`) | A banda `{minimum_rate, maximum_rate, default_rate}` da **sua** comissão por venda daquele produto. A comissão enviada na cotação e no pedido (`commission_data`) é validada contra essa faixa; se omitida, vale o `default_rate`. |
| **Configuração padrão** (`default_configuration`) | Valores padrão de cobertura configurados para a sua integração. Quando a seleção omite a lista `services`, a cotação preenche automaticamente todas as coberturas padrão configuradas. |

## Classificação dos produtos

Cada produto carrega uma classificação regulatória e comercial:

| Campo | Descrição |
|---|---|
| `product_category` | `insurance`, `capitalization` ou `benefit`. Um produto pode combinar coberturas de categorias diferentes (ex.: seguro + assistência). |
| `insurance_class` | O ramo SUSEP do produto (ex.: `credit_life`). Ausente para produtos não-seguro. Na cotação e no pedido, o ramo é retornado como objeto `{ name, class_number, group_number }`. |
| `regulator_registration` | O registro do produto na SUSEP (Código SUSEP). |

## Visibilidade e confidencialidade

- Somente produtos **ativos** habilitados para a sua integração são visíveis; um produto de outro parceiro (ou inexistente) retorna `404`.
- A faixa de comissão retornada é sempre a **sua** — nunca a de outro parceiro.
- As regras internas de tarifação e elegibilidade da seguradora não são expostas: você recebe os limites estruturais (espaço de opções e dependências) e o resultado da avaliação na [cotação](/documentation/seguros/cotacao/criar_cotacao).

## Endpoints

| Endpoint | Descrição |
|---|---|
| [`GET /v1/product_catalog/products`](/documentation/seguros/catalogo/listar_produtos) | Lista os produtos que a sua integração pode vender. |
| [`GET /v1/product_catalog/products/{product_key}`](/documentation/seguros/catalogo/consultar_produto) | Detalha um produto: coberturas, espaço de opções, dependências, seus padrões e sua faixa de comissão. |

---

# Listar produtos

URL: /documentation/seguros/catalogo/listar_produtos

Retorna a lista paginada de produtos de seguro **ativos e habilitados para a sua integração** — a resposta à pergunta "o que posso vender?". Cada item traz um resumo do produto e de suas coberturas; para o envelope completo de venda, use a [consulta de produto](/documentation/seguros/catalogo/consultar_produto).

## Request

ENDPOINT /v1/product_catalog/products
MÉTODO GET

### Query params

| Parâmetro | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `page` | integer | opcional | Número da página. Padrão: `1`. |
| `rows_per_page` | integer | opcional | Registros por página. Padrão: `50`. |
| `name` | string | opcional | Filtra pelo nome do produto. |
| `product_category` | string | opcional | Filtra pela categoria: `insurance`, `capitalization` ou `benefit`. |
| `insurance_class` | string | opcional | Filtra pelo ramo do produto (ex.: `credit_life`). |
| `service_category` | string | opcional | Filtra por produtos que contenham cobertura da categoria informada. |

```python title="Exemplo de chamada"
GET /v1/product_catalog/products?page=1&rows_per_page=50&product_category=insurance
```

## Response

STATUS 200

```json title="Response Body"
{
  "data": [
    {
      "product_key": "9d1f8c7a-3b21-4e60-8a2f-1c5d7e9b0a44",
      "name": "Prestamista Master",
      "product_category": "insurance",
      "insurance_class": "credit_life",
      "contract_instrument_type": "ticket",
      "provider_name": "QI Seguradora",
      "services": [
        {
          "service_type": "credit_life",
          "service_category": "insurance",
          "mandatory": true
        },
        {
          "service_type": "funeral_assistance",
          "service_category": "benefit",
          "mandatory": false
        }
      ]
    }
  ],
  "pagination": {
    "current_page": 1,
    "next_page": null,
    "rows_per_page": 50
  }
}
```

### Atributos da resposta

| Campo | Tipo | Descrição |
|---|---|---|
| `data` | array | Lista de produtos habilitados. |
| `pagination` | object | Estado da paginação: `current_page` (página atual), `next_page` (número da próxima página, ou `null` quando esta é a última) e `rows_per_page` (tamanho da página solicitado). |

:::caution Esta listagem não retorna `total`
A paginação do catálogo é **por cursor de página**, não por contagem: pare de paginar quando `pagination.next_page` vier `null`. A [listagem de pedidos](/documentation/seguros/pedidos/listar_pedidos) usa uma convenção diferente (`page_size` + `total`) — as duas superfícies não são intercambiáveis.
:::

#### Objeto em `data`

| Campo | Tipo | Descrição |
|---|---|---|
| `product_key` | string | Chave única do produto. Use-a na consulta de produto, na cotação e no pedido. |
| `name` | string | Nome comercial do produto. |
| `product_category` | string | Categoria do produto: `insurance`, `capitalization` ou `benefit`. |
| `insurance_class` | string | Ramo SUSEP do produto (ex.: `credit_life`). `null` para produtos não-seguro. |
| `contract_instrument_type` | string | Instrumento contratual do produto: `ticket` (bilhete) ou `policy` (apólice). Determina o fluxo de contratação — veja [Criar pedido](/documentation/seguros/pedidos/criar_pedido). `null` para produtos não securitários. |
| `provider_name` | string | Nome da seguradora parceira. |
| `services` | array | Resumo das coberturas do produto. |

#### Objeto em `services`

| Campo | Tipo | Descrição |
|---|---|---|
| `service_type` | string | Tipo da cobertura (ex.: `credit_life`, `life_death`, `funeral_assistance`). |
| `service_category` | string | Categoria da cobertura: `insurance`, `capitalization` ou `benefit`. |
| `mandatory` | boolean | Indica se a cobertura é obrigatória em toda venda do produto. |

## Possíveis erros

Todo erro (`non-2xx`) retorna o corpo padrão `{ "title", "description", "translation", "code" }` — trate programaticamente **apenas** o campo `code`.

| Status | Código | Descrição |
|---|---|---|
| `400` | `QIT000010` | Parâmetro de paginação inválido. |
| `400` | `QIT000001` | Parâmetro de filtro malformado. |
| `401` / `403` | — | Falha de autenticação ou autorização. |
| `429` | — | Limite de requisições excedido — repita com backoff. |
| `500` | `QIT000500` | Erro interno — seguro repetir a chamada. |
| `503` | `CAT000033` | Serviço indisponível — seguro repetir a chamada. |

---

# Criar cotação

URL: /documentation/seguros/cotacao/criar_cotacao

Precifica uma seleção de produtos e coberturas para um cliente. A cotação é uma **calculadora**: nada é persistido e nenhum recurso é criado. O [pedido](/documentation/seguros/pedidos/criar_pedido) usa exatamente a mesma lista `products[]` e é reprecificado com o mesmo motor no momento da submissão.

:::info A cotação é indicativa
Cotação e pedido rodam contra a configuração **vigente** — não há token de cotação, snapshot de tarifa nem prazo de validade. Se a tarifa ou a configuração do produto mudar entre a cotação e a submissão, o preço muda: **o preço calculado na submissão do pedido é o que vale**.
:::

Uma cotação pode combinar **vários produtos**, cada um segurando o seu próprio objeto de risco. O caso típico: um carro vendido com financiamento gera um pedido com o produto **prestamista** (objeto de risco = a operação de crédito) e o produto **auto** (objeto de risco = o veículo).

## Request

ENDPOINT /v1/insurance/quote
MÉTODO POST

Para **produtos de prateleira** — vendidos como estão, com as coberturas padrão e a comissão padrão da sua integração — a linha do produto precisa apenas de `product_key`, `term` e `risk_object`. É o cenário ideal para quem vende produtos fixos, sem personalização:

```json title="Request Body — produto de prateleira"
{
  "products": [
    {
      "product_key": "9d1f8c7a-3b21-4e60-8a2f-1c5d7e9b0a44",
      "term": {
        "start_date": "2026-07-16",
        "end_date": "2028-07-15"
      },
      "risk_object": {
        "type": "credit_operation",
        "insurable_value": 50000.00,
        "attributes": {
          "installment_amount": 1050.00,
          "number_of_installments": 48
        }
      }
    }
  ],
  "customer": {
    "date_of_birth": "1987-03-22",
    "occupation_code": "211205"
  }
}
```

Com `services` omitido, todas as coberturas com [configuração padrão](/documentation/seguros/catalogo/consultar_produto) da sua integração são preenchidas automaticamente; com `commission_data` omitido, vale o `default_rate` da faixa `commission_bounds` do produto.

Para personalizar a seleção — escolher coberturas, importância segurada, franquia, carência ou a comissão — envie `services` e `commission_data` explicitamente:

```json title="Request Body — seleção personalizada"
{
  "products": [
    {
      "product_key": "9d1f8c7a-3b21-4e60-8a2f-1c5d7e9b0a44",
      "commission_data": {
        "commission_type": "percentage_of_gross_premium",
        "value": 0.1000
      },
      "term": {
        "start_date": "2026-07-16",
        "end_date": "2028-07-15"
      },
      "services": [
        {
          "service_key": "0a3c5e7f-2b4d-4a6c-8e0f-1a3b5c7d9e2f",
          "insured_amount_basis": "monetary_amount",
          "insured_amount": 50000.00
        }
      ],
      "risk_object": {
        "type": "credit_operation",
        "insurable_value": 50000.00,
        "attributes": {
          "installment_amount": 1050.00,
          "number_of_installments": 48
        }
      }
    }
  ],
  "customer": {
    "date_of_birth": "1987-03-22",
    "occupation_code": "211205"
  }
}
```

### Atributos do request

| Campo | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `products` | array | obrigatório | A seleção de produtos a precificar, campo de topo do request — o mesmo formato usado no pedido. Um item por produto. |
| `customer` | object | opcional | Dados do segurado usados na precificação e na avaliação de elegibilidade, em objeto plano (sem wrapper). Quando omitido, a elegibilidade não é avaliada (`eligibility` retorna `not_evaluated`) — e produtos cuja tarifa depende de dados do cliente são rejeitados com `NOT_PRICEABLE`. |

#### Objeto em `products`

| Campo | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `product_key` | string | obrigatório | Chave do produto no [catálogo](/documentation/seguros/catalogo/listar_produtos). |
| `commission_data` | object | opcional | A forma de comissão desejada (veja abaixo). Quando omitida, vale o `default_rate` da faixa `commission_bounds` do produto. |
| `term` | object | obrigatório | Vigência do produto, em datas absolutas: `{ "start_date": "AAAA-MM-DD", "end_date": "AAAA-MM-DD" }`. **Obrigatório em todo item** — não há forma por duração nem vigência padrão da seleção. Produtos do mesmo pedido podem ter vigências diferentes. |
| `services` | array | opcional | Coberturas explícitas. Quando omitido ou vazio, todas as coberturas com [configuração padrão](/documentation/seguros/catalogo/consultar_produto) da sua integração são preenchidas automaticamente. |
| `risk_object` | object | condicional | O objeto que **este produto** segura. Obrigatório para produtos de risco valorado (`credit_operation`, `vehicle`); omitido quando o objeto do seguro é a própria pessoa (`person`). |

#### Objeto `commission_data`

| Campo | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `commission_type` | string | obrigatório | `percentage_of_gross_premium` (taxa sobre o prêmio bruto), `monetary_amount` (comissão em R$ fixo, valor-alvo) ou `total_gross_premium_amount` (preço final desejado ao cliente, valor-alvo). As três formas são mutuamente exclusivas. |
| `value` | number | obrigatório | O valor da forma escolhida: taxa com 4 casas (ex.: `0.1000`), valor em R$ (ex.: `61.73`) ou preço total (ex.: `650.00`). |

Em qualquer forma, a **taxa efetiva** resultante é validada contra a faixa `commission_bounds` do produto — violação rejeita a linha com `OUT_OF_BOUNDS_COMMISSION`. Nas formas por valor (`monetary_amount`, `total_gross_premium_amount`), o valor é um alvo: o realizado pode variar centavos por arredondamento.

#### Objeto em `services`

| Campo | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `service_key` | string | obrigatório | Chave da cobertura no catálogo. É o **único** campo obrigatório do item: omitindo os demais, vale a sua [configuração padrão](/documentation/seguros/catalogo/consultar_produto) para a cobertura. |
| `insured_amount_basis` | string | condicional | Base da importância segurada: `monetary_amount`, `percentage_of_risk_value` ou `unit_amount_times_count`. Informar a base torna obrigatório o campo de valor correspondente. |
| `insured_amount` | number | condicional | Importância segurada em reais. Obrigatório quando a base é `monetary_amount`. |
| `insured_amount_percentage` | number | condicional | Percentual do valor do objeto de risco, em `(0, 1]` (`1.0000` = 100%). Obrigatório quando a base é `percentage_of_risk_value`; resolvido contra o `insurable_value` do objeto de risco da linha. |
| `unit_amount` | number | condicional | Valor por unidade de indenização (ex.: R$ 100 por diária). Obrigatório, junto com `unit_count`, quando a base é `unit_amount_times_count`. Deve pertencer ao envelope `indemnity_unit_options` da cobertura. |
| `unit_count` | integer | condicional | Quantidade de unidades de indenização (ex.: 60 diárias). Obrigatório, junto com `unit_amount`, quando a base é `unit_amount_times_count`. |
| `deductible_data` | object | opcional | Franquia, na forma tipada `{ "deductible_type": "monetary_amount", "value": 1500.00 }`. O `deductible_type` deve ser o mesmo do envelope `deductible_options` da cobertura, e o valor deve pertencer ao espaço de opções. |
| `waiting_period_days` | integer | opcional | Carência em dias, dentro das `waiting_period_options` da cobertura. |

#### Objeto `risk_object`

| Campo | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `type` | string | obrigatório | Tipo do objeto de risco: `credit_operation`, `vehicle` ou `person`. |
| `insurable_value` | number | condicional | Valor do objeto de risco. Obrigatório para `credit_operation` e `vehicle`; não se aplica a `person`. Limita a importância segurada **das coberturas atreladas ao valor do risco** — coberturas de limite estipulado respeitam apenas o próprio `maximum_insured_amount`. |
| `attributes` | object | opcional | Atributos do objeto de risco usados na tarifação, específicos por ramo (ex.: para uma operação de crédito, `installment_amount` e `number_of_installments`). |

#### Objeto `customer`

O objeto é plano — não há wrapper `data`.

| Campo | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `document_number` | string | opcional | CPF ou CNPJ do segurado. |
| `name` | string | opcional | Nome completo. |
| `email` | string | opcional | E-mail. |
| `phone_number` | string | opcional | Telefone no formato E.164. |
| `date_of_birth` | string | opcional | Data de nascimento do segurado, no formato `YYYY-MM-DD`. A **idade** usada na precificação e nas regras de elegibilidade (ex.: idade máxima no fim da vigência) é derivada dela a cada chamada — não existe campo de idade. |
| `occupation_code` | string | opcional | Código de ocupação do segurado (CBO). |
| `address` | object | opcional | Endereço do segurado, na mesma forma usada no [pedido](/documentation/seguros/pedidos/criar_pedido#objeto-customeraddress). |

## Response

STATUS 200

```json title="Response Body"
{
  "total_order_amount": 617.28,
  "products": [
    {
      "product_key": "9d1f8c7a-3b21-4e60-8a2f-1c5d7e9b0a44",
      "name": "Prestamista Master",
      "provider_name": "QI Seguradora",
      "product_category": "insurance",
      "insurance_class": {
        "name": "credit_life",
        "class_number": "0977",
        "group_number": "09"
      },
      "contract_instrument_type": "ticket",
      "regulator_registration": "15414.900388/2015-21",
      "result": "priced",
      "term": {
        "start_date": "2026-07-16",
        "end_date": "2028-07-15"
      },
      "gross_premium_amount": 617.28,
      "iof_amount": 2.35,
      "net_premium_amount": 614.93,
      "services": [
        {
          "service_key": "0a3c5e7f-2b4d-4a6c-8e0f-1a3b5c7d9e2f",
          "service_type": {
            "code": "credit_life",
            "name": "Prestamista (Credit Life)"
          },
          "service_category": "insurance",
          "regulator_registration": null,
          "insured_amount": 50000.00,
          "unit_amount": null,
          "unit_count": null,
          "deductible_data": {
            "deductible_type": "monetary_amount",
            "value": 1500.00
          },
          "waiting_period_days": 30,
          "service_attributes": {},
          "gross_premium_amount": 617.28
        }
      ]
    }
  ],
  "eligibility": "eligible"
}
```

### Atributos da resposta

| Campo | Tipo | Descrição |
|---|---|---|
| `total_order_amount` | number | Soma dos prêmios brutos dos produtos precificados (`priced`), com IOF. |
| `products` | array | Resultado por produto. Cada linha é avaliada de forma independente: um produto rejeitado nunca contamina os demais na **cotação** (no pedido, qualquer linha rejeitada recusa a submissão inteira, que retorna erro `422`). |
| `eligibility` | string | Veredito de elegibilidade da cotação: `eligible`, `declined` (alguma regra de elegibilidade reprovou) ou `not_evaluated` (o `customer` não foi enviado — **não é uma aprovação**). |

#### Objeto em `products`

| Campo | Tipo | Descrição |
|---|---|---|
| `product_key` | string | Chave do produto avaliado. |
| `name` / `provider_name` | string | Nome comercial do produto e da seguradora. |
| `product_category` | string | Categoria do produto: `insurance`, `capitalization` ou `benefit`. |
| `insurance_class` | object | Ramo do seguro: `{ name, class_number, group_number }`. `null` para produtos não securitários. |
| `contract_instrument_type` | string | Instrumento contratual do produto: `ticket` (bilhete) ou `policy` (apólice). Ecoado aqui para que você conheça o instrumento — e portanto quais métodos de aceite são válidos — **antes** de coletar o aceite no [pedido](/documentation/seguros/pedidos/criar_pedido). |
| `regulator_registration` | string | Registro do produto no regulador. |
| `result` | string | `priced` (precificado) ou `rejected` (rejeitado). Presente em **toda** linha. |
| `term` | object | A vigência que você enviou, ecoada. Presente quando `priced`. |
| `gross_premium_amount` | number | Prêmio bruto do produto, com IOF — soma exata dos prêmios das coberturas. Presente quando `priced`. |
| `iof_amount` | number | IOF do produto. Presente quando `priced`. |
| `net_premium_amount` | number | Prêmio líquido do produto, sem IOF. Presente quando `priced`. |
| `services` | array | Coberturas precificadas, com o prêmio de cada uma. Presente quando `priced`. |
| `decline_reasons` | array | Motivos da rejeição, um item `{ code, detail }` por falha subjacente — duas coberturas da mesma linha violando o espaço de opções geram duas entradas sob o mesmo `code`. Presente **apenas** quando `rejected`. |

#### Objeto em `services`

| Campo | Tipo | Descrição |
|---|---|---|
| `service_key` | string | Chave da cobertura. |
| `service_type` | object | Tipo da cobertura: `{ code, name }`. |
| `service_category` | string | Categoria da cobertura: `insurance`, `capitalization` ou `benefit`. |
| `regulator_registration` | string | Registro SUSEP próprio da cobertura. `null` quando a cobertura herda o registro do produto. |
| `insured_amount` | number | Importância segurada resolvida — o percentual já aplicado sobre o valor do objeto de risco, o par por unidade já multiplicado. Você nunca a recalcula. |
| `unit_amount` | number | Valor por unidade de indenização. `null` quando a cobertura não é precificada por unidade. |
| `unit_count` | integer | Quantidade de unidades de indenização. `null` quando a cobertura não é precificada por unidade. |
| `deductible_data` | object | Franquia aplicada: `{ deductible_type, value }`. `null` quando a cobertura não tem franquia. |
| `waiting_period_days` | integer | Carência aplicada, em dias. `null` quando a cobertura **não tem** carência — que é diferente de uma carência de 0 dias. |
| `service_attributes` | object | Atributos fixos da cobertura, ecoados do catálogo. |
| `gross_premium_amount` | number | Prêmio bruto da cobertura, arredondado em 2 casas. |

### Motivos de rejeição

Quando `result` é `rejected`, a linha traz **apenas** `product_key`, `result` e `decline_reasons` — nada é oferecido, então nada é descrito: sem `name`, sem classificação, sem `term`, sem prêmios e sem `services`. Ramifique sempre pelo `result`, nunca pela presença de um campo.

A avaliação é por estágios (estrutura → precificação → elegibilidade): os motivos retornados são sempre do **mesmo estágio** — o primeiro que reprovar — coletados por completo.

```json title="Linha rejeitada"
{
  "product_key": "9d1f8c7a-3b21-4e60-8a2f-1c5d7e9b0a44",
  "result": "rejected",
  "decline_reasons": [
    {
      "code": "OUT_OF_OPTION_SPACE",
      "detail": "deductible 2000.00 is not in the option list for coverage credit_life"
    }
  ]
}
```

| Código | Significado |
|---|---|
| `NOT_ENTITLED` | A sua integração não está habilitada para vender este produto. |
| `INACTIVE_PRODUCT` | O produto está inativo (ou a chave é desconhecida). |
| `INACTIVE_SERVICE` | Uma cobertura selecionada está inativa. |
| `UNKNOWN_SERVICE` | Uma `service_key` não pertence a este produto. |
| `MISSING_MANDATORY_SERVICE` | A seleção omite uma cobertura obrigatória do produto. |
| `DEPENDENCY_VIOLATION` | A seleção viola as dependências entre coberturas (`include`/`exclude`). |
| `OUT_OF_OPTION_SPACE` | Importância segurada, franquia ou carência fora do espaço de opções da cobertura — inclusive tipo divergente do envelope, valor fora da `list`/`range`/`step`, ou valor enviado para uma cobertura sem franquia/carência (envelope `null`). |
| `INVALID_INSURED_AMOUNT_BASIS` | A base de importância segurada não é compatível com a linha — ex.: cobertura atrelada ao valor do risco em uma linha sem `insurable_value`. Vale para qualquer base. |
| `NOT_PRICEABLE` | O motor não conseguiu produzir um preço: um insumo da tarifa não é resolvível (ex.: a tarifa depende de dados do `customer` e ele não foi enviado). |
| `ZERO_PREMIUM` | A linha inteira precificou a custo zero. Uma única cobertura gratuita é válida (sai com `gross_premium_amount: 0.00`); a linha toda a zero é rejeitada. |
| `INELIGIBLE` | Uma regra de elegibilidade reprovou (ex.: idade máxima no fim da vigência). O `detail` do item nomeia a regra e os valores que a reprovaram. |
| `OUT_OF_BOUNDS_COMMISSION` | A comissão efetiva derivada de `commission_data` está fora da faixa `commission_bounds` do produto. |
| `DELEGATED_UNSUPPORTED` | Produto com precificação delegada à seguradora — reservado, ainda não suportado. |
| `STALE_DEFAULT` | Uma configuração padrão da sua integração aponta para uma cobertura que não está mais ativa — contate o suporte para atualizar o padrão. |

## Possíveis erros

Todo erro (`non-2xx`) retorna o corpo padrão `{ "title", "description", "translation", "code" }` — trate programaticamente **apenas** o campo `code`.

| Status | Código | Descrição |
|---|---|---|
| `400` | `QIT000001` | Requisição malformada: schema inválido, `products` vazio, produto sem `term`, base de importância segurada sem o campo de valor correspondente, `insurable_value` ausente para `credit_operation`/`vehicle`, `deductible_data` estruturalmente malformado, `customer.date_of_birth` fora do calendário ou no futuro. |
| `401` / `403` | — | Falha de autenticação ou autorização. |
| `429` | — | Limite de requisições excedido — repita com backoff. |
| `503` | — | Motor de precificação indisponível — a cotação falha rápido, sem preço em cache. Repita a chamada. |

---

# Início

URL: /documentation/seguros/cotacao/inicio

A etapa de precificação oferece duas operações, ambas **calculadoras**: nada é persistido e nenhum recurso é criado. A diferença está na pergunta que cada uma responde.

- A **cotação** (`POST /v1/insurance/quote`) responde *"quanto custa esta seleção?"* — calcula o **preço exato** de uma seleção de produtos e coberturas para uma configuração de comissão específica (a enviada em `commission_data`, ou o padrão do produto quando omitida).
- A **simulação** (`POST /v1/insurance/simulate`) responde *"por quanto eu posso vender esta seleção?"* — calcula a **faixa de preço vendável** de cada produto, variando apenas a comissão entre o mínimo e o máximo da sua faixa (`commission_bounds`). Por isso ela não aceita `commission_data`: a faixa inteira é varrida.

## Cotação vs. simulação

| | [Cotação](/documentation/seguros/cotacao/criar_cotacao) | [Simulação](/documentation/seguros/cotacao/simular_precos) |
|---|---|---|
| Responde | O preço exato da seleção. | O menor e o maior preço final possíveis por produto. |
| Comissão | Uma configuração específica (`commission_data` ou o padrão do produto). | Varre a faixa `commission_bounds` inteira — enviar `commission_data` é `400`. |
| Retorna | `gross_premium_amount` por produto e por cobertura, com veredito de elegibilidade. | `price_range` por produto: piso e teto do prêmio, comissão em R$ e taxas nos extremos. |
| Use para | Exibir o preço de uma oferta fechada antes de submeter o [pedido](/documentation/seguros/pedidos/criar_pedido). | Montar ofertas com preço customizado: descobrir os limites antes de escolher um `total_gross_premium_amount`. |

Os dois requests usam a mesma lista `products[]` do pedido — a única diferença estrutural é a presença ou não de `commission_data`.

## Fluxo típico

1. **Simule** a seleção para descobrir a faixa de preço vendável de cada produto.
2. Escolha o preço final dentro da faixa e **cote** com `commission_type: total_gross_premium_amount` para ver o preço exato e o veredito de elegibilidade.
3. **Submeta o pedido** com a mesma lista `products[]` — ele é reprecificado com o mesmo motor no momento da submissão.

:::info Preços são indicativos
Cotação e simulação rodam contra a configuração **vigente** — não há token de cotação, snapshot de tarifa nem prazo de validade. **O preço calculado na submissão do pedido é o que vale.**
:::

## Endpoints

| Endpoint | Descrição |
|---|---|
| [`POST /v1/insurance/quote`](/documentation/seguros/cotacao/criar_cotacao) | Precifica uma seleção com uma configuração de comissão específica. |
| [`POST /v1/insurance/simulate`](/documentation/seguros/cotacao/simular_precos) | Retorna a faixa de preço vendável de uma seleção, variando a comissão. |

---

# Simular faixa de preço

URL: /documentation/seguros/cotacao/simular_precos

Retorna a **faixa de preço vendável** de uma seleção — o menor e o maior preço final possíveis para cada produto, variando apenas a comissão dentro da sua faixa (`commission_bounds`). Use-a para montar ofertas com preço customizado: o valor enviado em `commission_data` com `commission_type: total_gross_premium_amount` no [pedido](/documentation/seguros/pedidos/criar_pedido) deve estar dentro dessa faixa.

Assim como a cotação, a simulação é uma calculadora: nada é persistido.

## Request

ENDPOINT /v1/insurance/simulate
MÉTODO POST

O request usa a mesma lista `products[]` da [cotação](/documentation/seguros/cotacao/criar_cotacao) — **sem** `commission_data`: a simulação varre a faixa de comissão inteira, então enviar uma comissão é `400`.

```json title="Request Body"
{
  "products": [
    {
      "product_key": "9d1f8c7a-3b21-4e60-8a2f-1c5d7e9b0a44",
      "term": {
        "start_date": "2026-07-16",
        "end_date": "2028-07-15"
      },
      "services": [
        {
          "service_key": "0a3c5e7f-2b4d-4a6c-8e0f-1a3b5c7d9e2f",
          "insured_amount_basis": "monetary_amount",
          "insured_amount": 50000.00
        }
      ],
      "risk_object": {
        "type": "credit_operation",
        "insurable_value": 50000.00,
        "attributes": {
          "installment_amount": 1050.00,
          "number_of_installments": 48
        }
      }
    }
  ],
  "customer": {
    "date_of_birth": "1987-03-22",
    "occupation_code": "211205"
  }
}
```

## Response

STATUS 200

```json title="Response Body"
{
  "total_order_floor_amount": 583.10,
  "total_order_ceiling_amount": 686.42,
  "products": [
    {
      "product_key": "9d1f8c7a-3b21-4e60-8a2f-1c5d7e9b0a44",
      "result": "priced",
      "name": "Prestamista Master",
      "provider_name": "QI Seguradora",
      "product_category": "insurance",
      "insurance_class": {
        "name": "credit_life",
        "class_number": "0977",
        "group_number": "09"
      },
      "contract_instrument_type": "ticket",
      "regulator_registration": "15414.900388/2015-21",
      "term": {
        "start_date": "2026-07-16",
        "end_date": "2028-07-15"
      },
      "price_range": {
        "floor_gross_premium_amount": 583.10,
        "ceiling_gross_premium_amount": 686.42,
        "floor_requester_amount": 29.16,
        "ceiling_requester_amount": 137.28,
        "minimum_requester_rate": 0.0500,
        "maximum_requester_rate": 0.2000
      },
      "services": [
        {
          "service_key": "0a3c5e7f-2b4d-4a6c-8e0f-1a3b5c7d9e2f",
          "service_type": {
            "code": "credit_life",
            "name": "Prestamista (Credit Life)"
          },
          "service_category": "insurance",
          "regulator_registration": null,
          "insured_amount": 50000.00,
          "unit_amount": null,
          "unit_count": null,
          "deductible_data": {
            "deductible_type": "monetary_amount",
            "value": 1500.00
          },
          "waiting_period_days": 30,
          "service_attributes": {}
        }
      ]
    }
  ],
  "eligibility": "eligible"
}
```

### Atributos da resposta

| Campo | Tipo | Descrição |
|---|---|---|
| `total_order_floor_amount` | number | Menor valor total possível do pedido (todas as comissões na taxa mínima). |
| `total_order_ceiling_amount` | number | Maior valor total possível do pedido (todas as comissões na taxa máxima). |
| `products` | array | Uma linha por produto solicitado, na ordem do request. Linhas rejeitadas são **byte a byte idênticas** às da [cotação](/documentation/seguros/cotacao/criar_cotacao#motivos-de-rejeicao) — `product_key`, `result: "rejected"` e `decline_reasons` — porque foram recusadas pelo mesmo motivo, no mesmo estágio. |
| `eligibility` | string | Veredito de elegibilidade da simulação: `eligible`, `declined` ou `not_evaluated`. |

#### Objeto em `products` (linha precificada)

A linha da simulação é **a linha da cotação com todos os campos de prêmio substituídos por uma única faixa**: mesma identidade, mesma classificação, mesmo `term` ecoado, mesmas coberturas realizadas — só o dinheiro muda. Ela traz `product_key`, `result`, `name`, `provider_name`, `product_category`, `insurance_class`, `contract_instrument_type`, `regulator_registration`, `term`, `price_range` e `services[]`.

`services[]` tem exatamente a forma da cotação **menos** o `gross_premium_amount` da cobertura: um prêmio por cobertura é uma decomposição no grão que a faixa deliberadamente não fixa. Todo o resto — importância segurada resolvida, par por unidade, franquia, carência e atributos — permanece, porque é o que você está vendendo.

#### Objeto `price_range`

| Campo | Tipo | Descrição |
|---|---|---|
| `floor_gross_premium_amount` | number | Menor preço final possível do produto (comissão na taxa mínima). |
| `ceiling_gross_premium_amount` | number | Maior preço final possível do produto (comissão na taxa máxima). |
| `floor_requester_amount` | number | A sua comissão em R$ no piso da faixa. |
| `ceiling_requester_amount` | number | A sua comissão em R$ no teto da faixa. |
| `minimum_requester_rate` | number | Taxa mínima da sua faixa de comissão para o produto. |
| `maximum_requester_rate` | number | Taxa máxima da sua faixa de comissão para o produto. |

Um `total_gross_premium_amount` igual ao piso ou ao teto da faixa usa exatamente a taxa `minimum_requester_rate`/`maximum_requester_rate`, sem arredondamento intermediário.

:::info Não há prêmio na simulação
A simulação **não** devolve `gross_premium_amount`, `iof_amount` nem `net_premium_amount`, em nenhum nível. Os extremos da faixa são os únicos valores de prêmio desta superfície. Para o preço fechado e decomposto, use a [cotação](/documentation/seguros/cotacao/criar_cotacao).
:::

## Possíveis erros

Todo erro (`non-2xx`) retorna o corpo padrão `{ "title", "description", "translation", "code" }` — trate programaticamente **apenas** o campo `code`.

| Status | Código | Descrição |
|---|---|---|
| `400` | `QIT000001` | Requisição malformada — inclusive `commission_data` presente em alguma linha. |
| `401` / `403` | — | Falha de autenticação ou autorização. |
| `429` | — | Limite de requisições excedido — repita com backoff. |
| `503` | — | Motor de precificação indisponível. Repita a chamada. |

---

# Consultar extrato

URL: /documentation/seguros/financeiro/consultar_extrato

:::caution Superfície ainda não disponível
Os endpoints financeiros descritos nesta seção **ainda não estão publicados** no gateway externo. Esta seção descreve o contrato-alvo e pode mudar antes da liberação — confirme a disponibilidade com o time de Integração ([api@qitech.com.br](mailto:api@qitech.com.br)) antes de implementar contra ela.
:::

Retorna o extrato paginado das movimentações da sua posição de comissão: créditos por parcela liquidada, estornos de cancelamento e liquidações de repasse. Cada movimentação de comissão é correlacionada à apólice que a originou pelo `policy_key`.

## Request

ENDPOINT /finance/v1/statement
MÉTODO GET

### Query params

| Parâmetro | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `page` | integer | opcional | Número da página. Padrão: `1`. |
| `page_size` | integer | opcional | Registros por página. Padrão: `50`. Máximo: `200`. |
| `from` | string | opcional | Data mínima da movimentação (`AAAA-MM-DD`). |
| `to` | string | opcional | Data máxima da movimentação (`AAAA-MM-DD`). |

```python title="Exemplo de chamada"
GET /finance/v1/statement?from=2026-07-01&to=2026-07-31&page=1&page_size=50
```

## Response

STATUS 200

```json title="Response Body"
{
  "items": [
    {
      "event_type": "REQUESTER_COMMISSION_BOOKED",
      "amount": "14.64",
      "policy_key": "0b6e7c1a-9a4e-4c1e-b1d4-2f5a8c9e0d31",
      "installment_number": 1,
      "occurred_at": "2026-07-20T14:05:01.000Z"
    },
    {
      "event_type": "REQUESTER_CLAWBACK_BOOKED",
      "amount": "-14.64",
      "policy_key": "0b6e7c1a-9a4e-4c1e-b1d4-2f5a8c9e0d31",
      "occurred_at": "2026-07-25T11:00:00.000Z"
    },
    {
      "event_type": "TRANSFER_SETTLED",
      "amount": "-980.10",
      "transfer_key": "3c4d5e6f-7a8b-4c9d-0e1f-2a3b4c5d6e7f",
      "occurred_at": "2026-07-21T09:00:12.000Z"
    }
  ],
  "page": 1,
  "page_size": 50,
  "total": 3
}
```

### Atributos da resposta

| Campo | Tipo | Descrição |
|---|---|---|
| `items` | array | Movimentações da sua posição, da mais recente para a mais antiga. |
| `page` | integer | Página atual. |
| `page_size` | integer | Tamanho da página solicitado. |
| `total` | integer | Total de movimentações no período. |

#### Objeto em `items`

| Campo | Tipo | Descrição |
|---|---|---|
| `event_type` | string | Tipo da movimentação. Veja a tabela de tipos abaixo. |
| `amount` | string | Valor **com sinal**: positivo credita a posição, negativo debita (estornos e liquidações de repasse). |
| `policy_key` | string | Apólice que originou a movimentação. Presente em créditos de comissão e estornos. |
| `installment_number` | integer | Número da parcela liquidada que originou o crédito. Presente em créditos de comissão. |
| `transfer_key` | string | Repasse correspondente. Presente em liquidações de repasse. |
| `occurred_at` | string | Instante da movimentação. |

### Tipos de movimentação

| `event_type` | Sinal | Significado |
|---|---|---|
| `REQUESTER_COMMISSION_BOOKED` | `+` | Comissão creditada sobre uma parcela de prêmio liquidada. |
| `REQUESTER_CLAWBACK_BOOKED` | `−` | Estorno de comissão pelo cancelamento de uma apólice com devolução de prêmio. |
| `TRANSFER_SETTLED` | `−` | Repasse liquidado na sua conta — a posição é debitada pelo valor transferido. |
| `MANUAL_ADJUSTMENT` | `+`/`−` | Ajuste operacional lançado pela QI Tech (ex.: resolução de incidente). |

## Possíveis erros

| Status | Descrição |
|---|---|
| `400` | Parâmetro de filtro ou paginação inválido. |
| `401` / `403` | Falha de autenticação ou autorização. |
| `500` / `503` | Erro interno ou serviço indisponível — seguro repetir a chamada. |

---

# Consultar saldo

URL: /documentation/seguros/financeiro/consultar_saldo

:::caution Superfície ainda não disponível
Os endpoints financeiros descritos nesta seção **ainda não estão publicados** no gateway externo. Esta seção descreve o contrato-alvo e pode mudar antes da liberação — confirme a disponibilidade com o time de Integração ([api@qitech.com.br](mailto:api@qitech.com.br)) antes de implementar contra ela.
:::

Retorna o saldo da sua posição de comissão: o valor pendente de liberação, o valor disponível para o próximo repasse e a data prevista da próxima transferência.

## Request

ENDPOINT /finance/v1/balance
MÉTODO GET

## Response

STATUS 200

```json title="Response Body"
{
  "account_key": "7a1b2c3d-0e4f-4a5b-8c6d-9e0f1a2b3c4d",
  "pending_amount": "1250.40",
  "available_amount": "980.10",
  "next_transfer_date": "2026-07-21"
}
```

### Atributos da resposta

| Campo | Tipo | Descrição |
|---|---|---|
| `account_key` | string | Chave da sua conta de comissão na QI Tech. |
| `pending_amount` | string | Total creditado que ainda não atingiu a data de liberação do cronograma de repasse. |
| `available_amount` | string | Valor líquido liberado (créditos menos estornos), que entrará no próximo repasse. |
| `next_transfer_date` | string | Data prevista do próximo repasse, conforme a cadência configurada para a sua conta. |

:::info Saldo negativo
Estornos de cancelamento podem deixar a posição temporariamente negativa. Nesse caso nenhum repasse é executado até que novos créditos compensem o saldo — a QI Tech nunca debita a sua conta.
:::

## Possíveis erros

| Status | Descrição |
|---|---|
| `401` / `403` | Falha de autenticação ou autorização. |
| `500` / `503` | Erro interno ou serviço indisponível — seguro repetir a chamada. |

---

# Início

URL: /documentation/seguros/financeiro/inicio

:::caution Superfície ainda não disponível
Os endpoints financeiros descritos nesta seção **ainda não estão publicados** no gateway externo. Esta seção descreve o contrato-alvo e pode mudar antes da liberação — confirme a disponibilidade com o time de Integração ([api@qitech.com.br](mailto:api@qitech.com.br)) antes de implementar contra ela.
:::

A superfície **Financeiro** dá visibilidade sobre a sua remuneração como distribuidor: o saldo da sua posição de comissão, o extrato de movimentações e os repasses realizados pela QI Tech para a sua conta.

## Como a sua comissão é apurada

A apuração é feita em **regime de caixa, por parcela de apólice**: a cada parcela de prêmio efetivamente liquidada pelo segurado, a sua comissão sobre aquela parcela é creditada na sua posição. Nada é creditado antes de o dinheiro entrar.

- **Crédito** — a cada parcela liquidada, a sua fatia (calculada com a taxa de comissão congelada na venda) vira um lançamento a pagar na sua posição.
- **Estorno (clawback)** — o cancelamento de uma apólice com devolução de prêmio gera um lançamento **negativo**, que compensa a comissão correspondente. Lançamentos negativos nunca geram cobrança contra a sua conta: eles são abatidos dos seus próximos créditos.
- **Repasse** — em uma cadência configurada para a sua conta (diária, semanal ou mensal, com valor mínimo opcional), a QI Tech agrega a posição líquida disponível e executa a transferência para a sua conta.

## Saldo pendente × disponível

| Conceito | Significado |
|---|---|
| `pending_amount` | Lançamentos creditados que ainda não atingiram a data de liberação do seu cronograma de repasse. |
| `available_amount` | Valor líquido já liberado, que entrará no próximo repasse. |

## Endpoints

| Endpoint | Descrição |
|---|---|
| [`GET /finance/v1/balance`](/documentation/seguros/financeiro/consultar_saldo) | Saldo da sua posição de comissão. |
| [`GET /finance/v1/statement`](/documentation/seguros/financeiro/consultar_extrato) | Extrato das suas movimentações. |
| [`GET /finance/v1/transfers`](/documentation/seguros/financeiro/listar_transferencias) | Repasses realizados para a sua conta. |

:::info
Os endpoints financeiros são somente de leitura: não existe endpoint de movimentação de dinheiro nesta superfície. Os repasses são executados automaticamente pela QI Tech conforme a configuração da sua conta.
:::

---

# Listar repasses

URL: /documentation/seguros/financeiro/listar_transferencias

:::caution Superfície ainda não disponível
Os endpoints financeiros descritos nesta seção **ainda não estão publicados** no gateway externo. Esta seção descreve o contrato-alvo e pode mudar antes da liberação — confirme a disponibilidade com o time de Integração ([api@qitech.com.br](mailto:api@qitech.com.br)) antes de implementar contra ela.
:::

Retorna a lista paginada dos repasses de comissão executados (ou em execução) para a sua conta.

## Request

ENDPOINT /finance/v1/transfers
MÉTODO GET

### Query params

| Parâmetro | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `page` | integer | opcional | Número da página. Padrão: `1`. |
| `page_size` | integer | opcional | Registros por página. Padrão: `50`. Máximo: `200`. |
| `status` | string | opcional | Filtra pelo status do repasse. |

```python title="Exemplo de chamada"
GET /finance/v1/transfers?status=SETTLED&page=1
```

## Response

STATUS 200

```json title="Response Body"
{
  "items": [
    {
      "transfer_key": "3c4d5e6f-7a8b-4c9d-0e1f-2a3b4c5d6e7f",
      "amount": "980.10",
      "status": "SETTLED",
      "scheduled_date": "2026-07-21",
      "settled_at": "2026-07-21T09:00:12.000Z"
    },
    {
      "transfer_key": "4d5e6f7a-8b9c-4d0e-1f2a-3b4c5d6e7f8a",
      "amount": "1250.40",
      "status": "PENDING",
      "scheduled_date": "2026-07-28",
      "settled_at": null
    }
  ],
  "page": 1,
  "page_size": 50,
  "total": 2
}
```

### Atributos da resposta

| Campo | Tipo | Descrição |
|---|---|---|
| `items` | array | Repasses, do mais recente para o mais antigo. |
| `page` | integer | Página atual. |
| `page_size` | integer | Tamanho da página solicitado. |
| `total` | integer | Total de repasses que atendem aos filtros. |

#### Objeto em `items`

| Campo | Tipo | Descrição |
|---|---|---|
| `transfer_key` | string | Chave única do repasse — a mesma referenciada nas movimentações `TRANSFER_SETTLED` do [extrato](/documentation/seguros/financeiro/consultar_extrato). |
| `amount` | string | Valor líquido do repasse (créditos carregados menos estornos compensados). |
| `status` | string | Status do repasse. Veja a tabela abaixo. |
| `scheduled_date` | string | Data programada da execução. |
| `settled_at` | string | Instante da liquidação. `null` enquanto não liquidado. |

### Status do repasse

| Status | Significado |
|---|---|
| `PENDING` | Repasse montado, aguardando execução na data programada. |
| `PROCESSING` | Instrução de transferência enviada, aguardando liquidação. |
| `SETTLED` | Liquidado na sua conta. Status terminal. |
| `FAILED` | A transferência falhou; será reprocessada. Os valores retornam à sua posição disponível até a nova tentativa. |

## Possíveis erros

| Status | Descrição |
|---|---|
| `400` | Parâmetro de filtro ou paginação inválido. |
| `401` / `403` | Falha de autenticação ou autorização. |
| `500` / `503` | Erro interno ou serviço indisponível — seguro repetir a chamada. |

---

# 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 apólice são notificados na URL de callback configurada para a sua integração. Para configurar a URL, siga [Configurando Webhooks](/documentation/seguros/primeiros_passos/seguros_configurando_webhooks) e valide o recebimento com [Validação de Webhooks](/documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_webhook_v2).

Os webhooks da QI Tech seguem uma estrutura padrão:

```json
{
  "webhook_type": "<tipo_do_evento>",
  "webhook_datetime": "2026-07-16T14:03:22Z",
  "data": {}
}
```

| Campo              | Tipo   | Descrição                                   |
| ------------------ | ------ | ------------------------------------------- |
| `webhook_type`     | string | Identificador do tipo de evento.            |
| `webhook_datetime` | string | Data e hora do evento no formato ISO 8601.  |
| `data`             | object | Dados específicos do evento.                |

Os eventos disponíveis nesta linha de produto estão descritos em [Webhooks de Pedido](/documentation/seguros/pedidos/webhooks) e [Webhooks de Apólice](/documentation/seguros/apolices/webhooks).

:::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.
:::

---

# 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).

---

# Cancelar pedido

URL: /documentation/seguros/pedidos/cancelar_pedido

Cancela um pedido. O comportamento depende do momento:

- **Antes da emissão** (`awaiting_payment`): a venda é desfeita e o pedido vai para `cancelled`.
- **Depois da emissão** (`emitted`): o pedido em si não muda de status. A chamada dispara **um pedido de cancelamento por apólice** do pedido; cada apólice segue o seu próprio [fluxo de cancelamento](/documentation/seguros/apolices/cancelar_apolice), incluindo o cálculo da devolução de prêmio.

## Request

ENDPOINT /v1/insurance/orders/{order_key}/cancel
MÉTODO POST

### Path params

| Parâmetro | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `order_key` | string | obrigatório | Chave do pedido. |

```json title="Request Body"
{
  "reason": "Cliente desistiu da compra"
}
```

| Campo | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `reason` | string | opcional | Motivo do cancelamento, registrado na trilha de eventos e propagado aos cancelamentos de apólice no caso pós-emissão. |

## Response

:::caution O QR Pix não é revogado no cancelamento pré-emissão
Nesta versão o cancelamento pré-emissão é uma transação local: o mandato Pix **não** é cancelado na cobrança, e o QR simplesmente expira junto com a janela de pagamento. Um segurado que autorize o pagamento mesmo assim paga em um pedido já `cancelled` — o pagamento é barrado antes da emissão e devolvido ao pagador. Trate o cancelamento como definitivo e pare de exibir o QR ao segurado.
:::

### Pré-emissão

STATUS 200

```json title="Response Body"
{
  "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
  "status": "cancelled"
}
```

### Pós-emissão

STATUS 202

```json title="Response Body"
{
  "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
  "status": "emitted",
  "policies_cancellation_requested": 2
}
```

| Campo | Tipo | Descrição |
|---|---|---|
| `policies_cancellation_requested` | integer | Quantidade de apólices cujo cancelamento foi solicitado — uma por produto do pedido. Acompanhe cada uma pelos [webhooks de apólice](/documentation/seguros/apolices/webhooks) ou pela [consulta de apólice](/documentation/seguros/apolices/consultar_apolice). |

### Semântica por status

| Status atual do pedido | Resultado |
|---|---|
| `awaiting_payment` | `200` — venda desfeita, pedido vai para `cancelled`. |
| `emitted` | `202` — cancelamento solicitado para cada apólice; o pedido permanece `emitted`. |
| `cancelled` | `200` — idempotente, nada muda. |
| `rejected` / `declined` / `expired` | `409` — status terminal, nada a cancelar. |

:::info Corrida entre pagamento e cancelamento
Se um pagamento for confirmado praticamente ao mesmo tempo do cancelamento pré-emissão, o pedido ainda é cancelado: o pagamento é barrado antes da emissão e devolvido ao pagador. Se o pagamento tiver sido confirmado **antes** e o pedido já estiver `emitted`, a mesma chamada passa a valer como cancelamento pós-emissão (`202`) — nunca há `409` nessa corrida.
:::

## Possíveis erros

Todo erro (`non-2xx`) retorna o corpo padrão `{ "title", "description", "translation", "code" }` — trate programaticamente **apenas** o campo `code`.

| Status | Código | Descrição |
|---|---|---|
| `401` / `403` | — | Falha de autenticação ou autorização. |
| `404` | `ORD000001` | Pedido inexistente ou pertencente a outra integração. |
| `409` | `ORD000010` | O pedido está em um status terminal sem nada a cancelar (`rejected`, `declined`, `expired`). |
| `502` / `504` | `ORD000031` | Falha em uma integração síncrona do cancelamento. Nenhum estado foi alterado — repita a chamada, ela é idempotente. |

---

# Consultar pedido

URL: /documentation/seguros/pedidos/consultar_pedido

Retorna o detalhe completo de um pedido: identidade, status, prazo de pagamento, os produtos congelados na submissão (com seus objetos de risco), a cobrança, o segurado e a trilha de eventos.

:::info O pedido não retorna apólices
O pedido é a visão da **venda**. Após a emissão (`emitted`), as apólices vivem em um recurso próprio — consulte-as com [`GET /v1/insurance/policies?order_key=`](/documentation/seguros/apolices/listar_apolices).
:::

## Request

ENDPOINT /v1/insurance/orders/{order_key}
MÉTODO GET

### Path params

| Parâmetro | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `order_key` | string | obrigatório | Chave do pedido. |

## Response

STATUS 200

```json title="Response Body"
{
  "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
  "status": "emitted",
  "distribution_type": "direct",
  "quote_data": {
    "total_order_amount": 617.28,
    "products": [
      {
        "order_product_key": "7f3a9c2e-0b5d-4e8a-a1c6-9d4b2e7f0a53",
        "product_key": "9d1f8c7a-3b21-4e60-8a2f-1c5d7e9b0a44",
        "product_category": "insurance",
        "insurance_class": {
          "name": "credit_life",
          "class_number": "0977",
          "group_number": "09"
        },
        "regulator_registration": "15414.900388/2015-21",
        "contract_instrument_type": "ticket",
        "gross_premium_amount": 617.28,
        "iof_amount": 2.35,
        "net_premium_amount": 614.93,
        "term": {
          "start_date": "2026-07-15",
          "end_date": "2027-07-14"
        },
        "risk_object": {
          "type": "credit_operation",
          "insurable_value": 50000.00,
          "attributes": {
            "installment_amount": 1050.00,
            "number_of_installments": 48
          }
        },
        "services": [
          {
            "service_key": "0a3c5e7f-2b4d-4a6c-8e0f-1a3b5c7d9e2f",
            "service_type": {
              "code": "credit_life",
              "name": "Prestamista (Credit Life)"
            },
            "service_category": "insurance",
            "regulator_registration": null,
            "gross_premium_amount": 617.28,
            "insured_amount": 50000.00,
            "unit_amount": null,
            "unit_count": null,
            "deductible_data": {
              "deductible_type": "monetary_amount",
              "value": 1500.00
            },
            "waiting_period_days": 30,
            "service_attributes": {},
            "term": {
              "start_date": "2026-07-15",
              "end_date": "2027-07-14"
            }
          }
        ]
      }
    ]
  },
  "payment_data": {
    "payment_method": "pix_automatic",
    "installment_count": 12,
    "installment_amount": 51.44,
    "first_installment_amount": 51.44,
    "first_due_date": "2026-07-23"
  },
  "customer": {
    "document_number": "96969879003",
    "name": "Maria Souza",
    "email": "maria@example.com",
    "phone_number": "+5511999990000",
    "date_of_birth": "1987-03-22",
    "occupation_code": "211205"
  },
  "events": [
    {
      "new_status": "emitted",
      "at": "2026-07-16T14:03:22Z"
    }
  ]
}
```

### Atributos da resposta

| Campo | Tipo | Descrição |
|---|---|---|
| `order_key` | string | Chave única do pedido. |
| `status` | string | Status atual. Veja o [ciclo de vida](/documentation/seguros/pedidos/inicio). |
| `expires_at` | string | Prazo para o pagamento da primeira parcela (7 dias a partir da submissão). `null` em um pedido `rejected`. |
| `distribution_type` | string | Modelo de distribuição da venda. Hoje sempre `direct`. |
| `payment_data` | object | A cobrança do pedido — mesmo bloco retornado na [criação](/documentation/seguros/pedidos/criar_pedido#objeto-payment_data-resposta). O sub-objeto `payment_artifact` (o QR Pix) só é reexposto enquanto o pedido está `awaiting_payment`; em um pedido emitido ou terminal ele é omitido. `null` em um pedido `rejected`. |
| `quote_data` | object | Os produtos congelados na submissão — mesmo bloco, com a mesma forma, retornado na [criação do pedido](/documentation/seguros/pedidos/criar_pedido). |
| `quote_data.total_order_amount` | number | Valor total do pedido (soma dos prêmios brutos dos produtos). |
| `customer` | object | O segurado do pedido, em objeto plano: `document_number`, `name`, `email`, `phone_number`, `date_of_birth`, `occupation_code`. Devolvido exatamente como foi submetido — a data de nascimento é o dado congelado; a idade usada na precificação foi derivada dela na submissão e não é armazenada. Os campos opcionais (`occupation_code`, `address`) **só aparecem se tiverem sido enviados**: nada é preenchido por padrão, e a chave é omitida em vez de vir `null`. |
| `events` | array | Trilha de mudanças de status do pedido, em ordem cronológica. Cada entrada é `{ "new_status", "at" }`. |

#### Objeto em `quote_data.products`

| Campo | Tipo | Descrição |
|---|---|---|
| `order_product_key` | string | Chave do produto dentro do pedido — a correlação com a apólice gerada na emissão. |
| `product_key` | string | Chave do produto no catálogo. |
| `product_category` | string | Categoria do produto: `insurance`, `capitalization` ou `benefit`. |
| `insurance_class` | object | Ramo do seguro: `{ name, class_number, group_number }`. `null` para produtos não securitários. |
| `regulator_registration` | string | Registro do produto no regulador (ex.: processo SUSEP). |
| `contract_instrument_type` | string | Instrumento contratual congelado do produto: `ticket` (bilhete) ou `policy` (apólice). `null` para produtos não securitários. |
| `gross_premium_amount` | number | Prêmio bruto do produto (com IOF). |
| `iof_amount` | number | IOF do produto. |
| `net_premium_amount` | number | Prêmio líquido do produto (sem IOF). |
| `term` | object | Vigência do produto, congelada na submissão: `{ start_date, end_date }`. Produtos do mesmo pedido podem ter vigências diferentes. |
| `risk_object` | object | O objeto de risco congelado deste produto (`type`, `insurable_value`, `attributes`). |
| `services` | array | Coberturas congeladas — cada uma com `service_type` (`{ code, name }`), `service_category`, `regulator_registration`, prêmio bruto, importância segurada, o par por unidade (`unit_amount` / `unit_count`, `null` quando a cobertura não é precificada por unidade), franquia (`deductible_data`), carência (`waiting_period_days`), atributos e vigência (`term`). |

:::caution Pedido `rejected` na consulta
Um pedido nascido `rejected` não persiste produtos nem cobrança: a consulta devolve `quote_data.products` vazio e `payment_data` nulo, e **não** repete os `decline_reasons`. Os motivos da recusa são entregues uma única vez, no `201` da [submissão](/documentation/seguros/pedidos/criar_pedido#pedido-recusado-rejected) — registre-os no seu lado.
:::

## Possíveis erros

Todo erro (`non-2xx`) retorna o corpo padrão `{ "title", "description", "translation", "code" }` — trate programaticamente **apenas** o campo `code`.

| Status | Código | Descrição |
|---|---|---|
| `401` / `403` | — | Falha de autenticação ou autorização. |
| `404` | `ORD000001` | Pedido inexistente ou pertencente a outra integração — os casos são indistinguíveis. |
| `500` / `503` | `QIT000500` | Erro interno ou serviço indisponível — seguro repetir a chamada. |

---

# Criar pedido

URL: /documentation/seguros/pedidos/criar_pedido

Cria e submete um pedido em uma única chamada. A submissão reprecifica a seleção no servidor (preços enviados pelo cliente nunca são confiados), valida o **aceite** que você coletou do segurado, cria a cobrança do prêmio e devolve o pedido em `awaiting_payment`, com o artefato Pix que o segurado deve pagar.

A lista `products[]` é **a mesma da [cotação](/documentation/seguros/cotacao/criar_cotacao)** — cotação e pedido usam a mesma gramática de seleção, acrescida do bloco `acceptance` por produto. A cotação é **indicativa**: a submissão reprecifica contra a configuração vigente e o preço do submit é o que vale.

:::info O aceite é coletado por você
A QI Tech não renderiza documento de proposta nem hospeda tela de assinatura nesta versão. Você conduz a cerimônia de aceite no seu próprio fluxo e **atesta** o resultado no campo `acceptance` de cada produto. Não existe `acceptance_url`.
:::

:::caution Apenas bilhete (`ticket`) nesta versão
O produto informa em `contract_instrument_type` se é vendido como **bilhete** (`ticket`) ou como **apólice** (`policy`). O bilhete dispensa proposta — o contrato se forma pelo ato da compra, e é por isso que o aceite atestado por você basta. Produtos `policy` ainda **não são vendáveis** e são recusados com `ORD000033`. O campo é ecoado na [cotação](/documentation/seguros/cotacao/criar_cotacao), então você descobre o instrumento **antes** de coletar o aceite.
:::

## Request

ENDPOINT /v1/insurance/order
MÉTODO POST

```json title="Request Body"
{
  "request_control_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
  "products": [
    {
      "product_key": "9d1f8c7a-3b21-4e60-8a2f-1c5d7e9b0a44",
      "commission_data": {
        "commission_type": "percentage_of_gross_premium",
        "value": 0.1000
      },
      "term": {
        "start_date": "2026-07-16",
        "end_date": "2027-07-15"
      },
      "services": [
        {
          "service_key": "0a3c5e7f-2b4d-4a6c-8e0f-1a3b5c7d9e2f",
          "insured_amount_basis": "monetary_amount",
          "insured_amount": 50000.00
        }
      ],
      "risk_object": {
        "type": "credit_operation",
        "insurable_value": 50000.00,
        "attributes": {
          "installment_amount": 1050.00,
          "number_of_installments": 48
        }
      },
      "acceptance": {
        "acceptance_method": "click_wrap",
        "accepted_at": "2026-07-16T13:58:04Z",
        "ip_address": "200.150.10.24",
        "document_number_hash": "f7c3bc1d808e04732adf679965ccc34ca7ae3441ef0d5e6ba2c1d0f0d5f1e2a3",
        "terms": {
          "version": "2026-05-v3",
          "hash": "9b74c9897bac770ffc029102a200c5de"
        },
        "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X)",
        "evidence_reference": "ceremony-8842197"
      }
    }
  ],
  "customer": {
    "document_number": "96969879003",
    "name": "Maria Souza",
    "email": "maria@example.com",
    "phone_number": "+5511999990000",
    "date_of_birth": "1987-03-22",
    "occupation_code": "211205",
    "address": {
      "street": "Avenida Paulista",
      "number": "1000",
      "complement": "Conjunto 42",
      "neighborhood": "Bela Vista",
      "city": "São Paulo",
      "state": "SP",
      "postal_code": "01310100"
    }
  },
  "payment_data": {
    "payment_method": "pix_automatic",
    "installment_count": 12
  }
}
```

### Atributos do request

| Campo | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `request_control_key` | string (UUID) | opcional | Chave de controle definida por você. Veja [Controle de duplicidade](#controle-de-duplicidade) — **não é uma chave de retentativa**. |
| `products` | array | obrigatório | A seleção de produtos, de 1 a 20 itens, no mesmo formato da [cotação](/documentation/seguros/cotacao/criar_cotacao#atributos-do-request) mais o bloco `acceptance`. |
| `customer` | object | obrigatório | O comprador/segurado do pedido (um por pedido). |
| `payment_data` | object | obrigatório | Forma de pagamento do prêmio. |

#### Objeto em `products[]`

| Campo | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `product_key` | string | obrigatório | Chave do produto no [catálogo](/documentation/seguros/catalogo/inicio). Não pode se repetir no mesmo pedido. |
| `commission_data` | object | opcional | A forma de comissão desejada para o produto. Omitido, aplica-se a taxa padrão (`default_rate`) do produto. Veja abaixo. |
| `term` | object | obrigatório | Vigência do produto, em datas absolutas: `{ "start_date": "AAAA-MM-DD", "end_date": "AAAA-MM-DD" }`. Não há forma por duração nem vigência padrão no nível do pedido. `end_date` deve ser posterior a `start_date`. |
| `services` | array | opcional | Coberturas selecionadas. Omitido, aplica-se a configuração padrão do produto. Mesmo formato da [cotação](/documentation/seguros/cotacao/criar_cotacao#objeto-em-services). |
| `risk_object` | object | obrigatório | Objeto de risco do produto (`type`, `insurable_value`, `attributes`). `insurable_value` é obrigatório para `type` `credit_operation` e `vehicle`. Um produto cujo objeto de risco é a própria pessoa informa `{"type": "person"}` — o bloco nunca é omitido. |
| `acceptance` | object | obrigatório | O aceite do segurado para **este** produto. Veja abaixo. |

#### Objeto `commission_data`

| Campo | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `commission_type` | string | obrigatório | Uma de três formas, mutuamente exclusivas: `percentage_of_gross_premium` (taxa sobre o prêmio bruto), `monetary_amount` (comissão em R$ fixo) ou `total_gross_premium_amount` (preço final desejado ao cliente). |
| `value` | number | obrigatório | O valor da forma escolhida: taxa com 4 casas decimais (ex.: `0.1000`), valor em R$ (ex.: `61.73`) ou preço total (ex.: `650.00`). |

A comissão efetiva é sempre validada contra a faixa (`commission_bounds`) do produto. Um valor fora da faixa **rejeita a linha na precificação**: o pedido nasce `rejected`, com `OUT_OF_BOUNDS_COMMISSION` em `decline_reasons`. Um `commission_type` desconhecido ou `value` mal tipado é `400`.

#### Objeto `acceptance`

O aceite é atestado **por produto**, nunca herdado de um produto vizinho: cada `OrderProduct` vira exatamente uma apólice, e a evidência precisa sobreviver ao lado do contrato que ela justifica. Quando uma única cerimônia cobriu vários produtos, repita o bloco em cada item.

| Campo | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `acceptance_method` | string | obrigatório | Como o aceite foi coletado: `click_wrap`, `checkbox`, `otp_sms`, `otp_email` ou `voice`. |
| `accepted_at` | string | obrigatório | Data e hora do aceite, em RFC 3339 (ex.: `2026-07-16T13:58:04Z`). Deve estar dentro das últimas **24 horas** e não pode estar no futuro além de 5 minutos de tolerância de relógio — caso contrário, `ORD000027`. |
| `ip_address` | string | obrigatório | Endereço IP (v4 ou v6) de onde o aceite foi dado. |
| `document_number_hash` | string | obrigatório | SHA-256 (hex, 64 caracteres) do `customer.document_number`, sem salt. É recalculado e conferido no servidor: divergência é `ORD000026`. |
| `terms` | object | obrigatório | Identificação das condições aceitas: `{ "version", "hash" }`. |
| `user_agent` | string | opcional | User agent do dispositivo do segurado (até 512 caracteres). |
| `evidence_reference` | string | opcional | Referência da evidência no seu sistema (até 128 caracteres). |

:::caution O hash é um token de integridade, não anonimização
`document_number_hash` é SHA-256 **sem salt** sobre um CPF — o espaço é exaustivamente pesquisável. A escolha é deliberada, para que qualquer detentor do documento possa reverificar o vínculo. Não o trate como dado pseudonimizado.
:::

#### Objeto `customer`

O objeto é plano — não há wrapper `data`. Diferente da cotação, onde tudo é opcional, aqui o comprador é o **segurado de registro** e também o pagador da cobrança — por isso a maioria dos campos passa a ser obrigatória.

| Campo | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `document_number` | string | obrigatório | CPF (11 dígitos) ou CNPJ (14 dígitos) do segurado, somente dígitos. |
| `name` | string | obrigatório | Nome completo. |
| `email` | string | obrigatório | E-mail do segurado. |
| `phone_number` | string | obrigatório | Telefone, 10 a 15 dígitos, opcionalmente prefixado por `+`. |
| `date_of_birth` | string | obrigatório | Data de nascimento do segurado, no formato `YYYY-MM-DD`. A **idade** lida pelas regras de precificação e elegibilidade é derivada dela no momento da cotação — não envie idade. |
| `occupation_code` | string | opcional | Código de ocupação (CBO). Se o produto precifica ou avalia elegibilidade por ocupação, a ausência do código **rejeita a linha** — o pedido nasce `rejected` com o motivo em `decline_reasons`, não `400`. Consulte o produto no [catálogo](/documentation/seguros/catalogo/inicio) para saber se ele lê esse campo. |
| `address` | object | obrigatório | Endereço do segurado. |

#### Objeto `customer.address`

| Campo | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `street` | string | obrigatório | Logradouro. |
| `number` | string | obrigatório | Número. |
| `complement` | string | opcional | Complemento. |
| `neighborhood` | string | obrigatório | Bairro. |
| `city` | string | obrigatório | Município. |
| `state` | string | obrigatório | Unidade federativa. |
| `postal_code` | string | obrigatório | CEP, 8 dígitos, sem separadores. |

#### Objeto `payment_data`

| Campo | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `payment_method` | string | obrigatório | Meio de pagamento do prêmio. Único valor aceito nesta versão: `pix_automatic` (Pix Automático). |
| `installment_count` | integer | obrigatório | Quantidade de parcelas do prêmio, de `1` a `24`. |

### Controle de duplicidade

`request_control_key` é uma **asserção de unicidade**, não um handle de retentativa. **Qualquer** segundo uso do mesmo par (sua integração, `request_control_key`) responde `409` / `ORD000011` — inclusive com corpo idêntico. Nada compara corpos.

:::danger Obrigação de integração
Um submit que estourar timeout pode ter sido **efetivado**. Repetir a chamada com a mesma `request_control_key` responde `409`, e não devolve o pedido. O caminho de recuperação é `GET /v1/insurance/order?request_control_key={sua_chave}`. Repetir com uma **chave nova** vende a mesma coisa duas vezes.
:::

## Response

STATUS 201

:::info O `POST` responde `201` **sempre**
Uma recusa de negócio é um recurso criado, não uma falha da chamada: o pedido nasce com `status: "rejected"` e os motivos em `decline_reasons`, ainda em `201`. **Ramifique pelo `status`, nunca pela classe HTTP.** O envelope de erro fica reservado para chamadas que não chegaram a uma decisão.
:::

### Pedido criado (`awaiting_payment`)

```json title="Response Body — pedido criado"
{
  "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
  "status": "awaiting_payment",
  "distribution_type": "direct",
  "expires_at": "2026-07-23T13:58:04Z",
  "quote_data": {
    "total_order_amount": 617.28,
    "products": [
      {
        "order_product_key": "7f3a9c2e-0b5d-4e8a-a1c6-9d4b2e7f0a53",
        "product_key": "9d1f8c7a-3b21-4e60-8a2f-1c5d7e9b0a44",
        "product_category": "insurance",
        "insurance_class": {
          "name": "credit_life",
          "class_number": "0977",
          "group_number": "09"
        },
        "regulator_registration": "15414.900388/2015-21",
        "contract_instrument_type": "ticket",
        "gross_premium_amount": 617.28,
        "iof_amount": 2.35,
        "net_premium_amount": 614.93,
        "term": {
          "start_date": "2026-07-16",
          "end_date": "2027-07-15"
        },
        "risk_object": {
          "type": "credit_operation",
          "insurable_value": 50000.00,
          "attributes": {
            "installment_amount": 1050.00,
            "number_of_installments": 48
          }
        },
        "services": [
          {
            "service_key": "0a3c5e7f-2b4d-4a6c-8e0f-1a3b5c7d9e2f",
            "service_type": {
              "code": "credit_life",
              "name": "Prestamista (Credit Life)"
            },
            "service_category": "insurance",
            "regulator_registration": null,
            "gross_premium_amount": 617.28,
            "insured_amount": 50000.00,
            "unit_amount": null,
            "unit_count": null,
            "deductible_data": {
              "deductible_type": "monetary_amount",
              "value": 1500.00
            },
            "waiting_period_days": 30,
            "service_attributes": {},
            "term": {
              "start_date": "2026-07-16",
              "end_date": "2027-07-15"
            }
          }
        ]
      }
    ]
  },
  "payment_data": {
    "payment_method": "pix_automatic",
    "installment_count": 12,
    "installment_amount": 51.44,
    "first_installment_amount": 51.44,
    "first_due_date": "2026-07-23",
    "payment_artifact": {
      "type": "pix_automatic",
      "qr_code_payload": "https://pix.example.qitech.app/r/9f2c1b0e",
      "qr_code_key": "9f2c1b0e-5d47-4a11-9c3e-0b8a7d61f402"
    }
  },
  "customer_document_number": "96969879003"
}
```

| Campo | Tipo | Descrição |
|---|---|---|
| `order_key` | string | Chave única do pedido. |
| `status` | string | `awaiting_payment` no pedido criado. Veja o [ciclo de vida](/documentation/seguros/pedidos/inicio). |
| `distribution_type` | string | Modelo de distribuição da venda. Hoje sempre `direct`. |
| `expires_at` | string | Prazo para o pagamento da primeira parcela: 7 dias a partir da submissão. Vencido o prazo, o pedido expira. |
| `quote_data` | object | O que foi vendido e congelado na submissão: `total_order_amount` e produtos com suas coberturas. É o mesmo bloco, com a mesma forma, retornado na [consulta do pedido](/documentation/seguros/pedidos/consultar_pedido). |
| `quote_data.products[].order_product_key` | string | Chave do produto **dentro do pedido**. É a chave de correlação com a apólice gerada na emissão. |
| `payment_data` | object | A cobrança criada para o pedido. Veja abaixo. |
| `customer_document_number` | string | Documento do segurado. |

#### Objeto `payment_data` (resposta)

| Campo | Tipo | Descrição |
|---|---|---|
| `payment_method` | string | Meio de pagamento congelado — `pix_automatic`. |
| `installment_count` | integer | Quantidade de parcelas. |
| `installment_amount` | number | Valor de cada parcela. |
| `first_installment_amount` | number | Valor da primeira parcela — absorve o resíduo de arredondamento, de modo que `first_installment_amount + (installment_count - 1) × installment_amount` reconcilia exatamente com `total_order_amount`. |
| `first_due_date` | string | Vencimento da primeira parcela (`AAAA-MM-DD`). |
| `payment_artifact` | object | O artefato Pix a ser entregue ao segurado: `type`, `qr_code_payload` (a **URL** do Pix, não o copia-e-cola EMV) e `qr_code_key`. **Retornado apenas enquanto o pedido está `awaiting_payment`** — em um pedido emitido ou terminal o QR está gasto e não é reexposto. |

### Pedido recusado (`rejected`)

Quando a precificação recusa qualquer linha, o pedido nasce `rejected`. A submissão é **tudo-ou-nada**: uma linha recusada recusa o pedido inteiro, nenhuma cobrança é criada e nenhum produto é persistido. Mesmo assim o recurso existe e é consultável pelo `order_key`.

```json title="Response Body — pedido recusado (201)"
{
  "order_key": "b41d90a7-8c22-4f3e-9a10-2d6e4b7c5f81",
  "status": "rejected",
  "decline_reasons": [
    {
      "product_key": "9d1f8c7a-3b21-4e60-8a2f-1c5d7e9b0a44",
      "code": "INELIGIBLE",
      "detail": "age_at_maturity 76 exceeds the maximum 75 for coverage credit_life"
    }
  ]
}
```

| Campo | Tipo | Descrição |
|---|---|---|
| `order_key` | string | Chave do pedido recusado. |
| `status` | string | Sempre `rejected` neste ramo. |
| `decline_reasons` | array | Lista plana de `{ product_key, code, detail }`, uma entrada por falha subjacente — duas coberturas de um mesmo produto violando o espaço de opções geram duas entradas sob o mesmo `code`. Os códigos são os [mesmos da cotação](/documentation/seguros/cotacao/criar_cotacao#motivos-de-rejeicao). `product_key` pode ser `null` para um motivo não atribuível a um produto específico. |

Um pedido `rejected` **não** traz `quote_data` nem `payment_data`: nada foi vendido e não há o que pagar.

## Possíveis erros

Todo erro (`non-2xx`) retorna o corpo padrão `{ "title", "description", "translation", "code" }` — trate programaticamente **apenas** o campo `code`. Recusa de negócio **não** vem por aqui: ela é o `201` com `status: rejected` descrito acima.

| Status | Código | Descrição |
|---|---|---|
| `400` | `QIT000001` | Requisição malformada (schema inválido) — inclusive `customer.date_of_birth` fora do calendário ou no futuro. |
| `401` / `403` | — | Falha de autenticação ou autorização. |
| `409` | `ORD000011` | `request_control_key` já utilizada pela sua integração. Recupere o pedido com `GET /v1/insurance/order?request_control_key=`. |
| `422` | `ORD000020` | Objeto de risco ausente, ou sem `insurable_value` quando o tipo o exige (`credit_operation`/`vehicle`). |
| `422` | `ORD000021` | Produto sem `term`. |
| `422` | `ORD000022` | `term.end_date` anterior ou igual a `term.start_date`. |
| `422` | `ORD000023` | `product_key` repetido no mesmo pedido. |
| `422` | `ORD000024` | Integração inativa — não pode transacionar. |
| `422` | `ORD000025` | Produto sem o bloco `acceptance`. |
| `422` | `ORD000026` | `acceptance.document_number_hash` não corresponde ao `customer.document_number` do pedido. |
| `422` | `ORD000027` | `acceptance.accepted_at` inválido, no futuro ou fora da janela de 24 horas. |
| `422` | `ORD000028` | A sua integração não tem configuração de pagamento e não pode ser cobrada. Contate o time de Integração. |
| `422` | `ORD000029` | O meio de pagamento solicitado não está habilitado para a sua integração. |
| `422` | `ORD000032` | O pedido mistura instrumentos contratuais diferentes — bilhete e apólice não podem ser vendidos no mesmo pedido. |
| `422` | `ORD000033` | O instrumento contratual do produto não está disponível para venda (apenas `ticket` nesta versão). |
| `502` / `504` | `ORD000031` | Falha em uma integração síncrona da submissão (cobrança). Nenhum pedido foi criado. |
| `503` | `ORD000030` | Motor de precificação indisponível — as vendas ficam pausadas. Repita a chamada. |

:::caution Retentativa após `502` / `504` / `503`
Repetir o submit exige **a mesma** `request_control_key` — ou nenhuma. Uma chave nova cria um segundo pedido para a mesma venda.
:::

---

# Início

URL: /documentation/seguros/pedidos/inicio

O **pedido** (`order`) é a unidade de venda do Insurance-as-a-Service: uma submissão que carrega um ou mais produtos, o segurado, o **aceite** que você coletou dele e a forma de pagamento. O pedido nasce aguardando o **pagamento** da primeira parcela. Confirmado o pagamento, o pedido é **emitido** — e cada produto vendido vira uma [apólice](/documentation/seguros/apolices/inicio).

:::info O aceite viaja no próprio pedido
Não há etapa de assinatura hospedada pela QI Tech nesta versão. Você conduz a cerimônia de aceite no seu fluxo e atesta o resultado no bloco `acceptance` de cada produto da submissão. Por isso o pedido nasce já em `awaiting_payment`, e não em um estado de espera por assinatura.
:::

## Ciclo de vida do pedido

![Fluxo de status do pedido, da submissão à emissão, com os desfechos possíveis](/img/diagrams/seguros-pedidos-inicio.svg)

_Como ler o diagrama: **azul** = status intermediário · **verde** = emitido (desfecho de sucesso) · **vermelho** = desfecho sem emissão. Toda transição de status gera um [webhook](/documentation/seguros/pedidos/webhooks)._

| Status | Significado |
|---|---|
| `awaiting_payment` | Pedido criado e precificado; aguardando a confirmação do pagamento da primeira parcela via o `payment_artifact` Pix retornado na submissão. |
| `emitted` | Pagamento confirmado; as apólices do pedido foram disparadas para emissão. Status terminal do pedido — daqui em diante o acompanhamento é pelas [apólices](/documentation/seguros/apolices/inicio). |
| `rejected` | O pedido nasceu rejeitado na submissão porque a precificação recusou ao menos uma linha. Os motivos vêm em `decline_reasons`, no próprio `201`. |
| `expired` | O prazo de 7 dias para pagamento (`expires_at`) venceu sem confirmação. |
| `cancelled` | O pedido foi cancelado pela sua integração antes da emissão. |

### Status reservados

Os enumeradores abaixo existem no modelo de dados mas **não ocorrem nesta versão**. Eles são a razão pela qual você não deve mapear o campo `status` de forma fechada — trate um valor desconhecido como "em andamento" e consulte o pedido.

| Status | Reservado para |
|---|---|
| `awaiting_acceptance` | O fluxo de **apólice** (`contract_instrument_type: policy`), que exige proposta renderizada e assinatura sobre ela. |
| `under_analysis` | Análise cadastral assíncrona (KYC). |
| `declined` | Recusa do segurado em uma cerimônia de assinatura conduzida pela QI Tech. |

:::info O pagamento sempre vence o relógio
A expiração nunca desfaz um pagamento válido: se o pagamento for confirmado enquanto a expiração está sendo processada, o pedido é emitido normalmente. Um pagamento que chegue **depois** de um cancelamento é devolvido ao pagador automaticamente.
:::

## O que congela na submissão

No momento da submissão, o pedido congela tudo o que foi precificado: produtos, coberturas, prêmios, importâncias seguradas, vigências, objetos de risco e o aceite atestado. Esses dados são imutáveis e são exatamente o que as apólices herdarão na emissão — uma reprecificação posterior do catálogo nunca afeta um pedido já submetido.

## Pedido × apólice

- Um pedido vende **N produtos**; a emissão gera **uma apólice por produto**, correlacionada pelo `order_product_key`.
- O pedido **não** retorna apólices nas consultas: descubra as apólices de um pedido emitido com [`GET /v1/insurance/policies?order_key=`](/documentation/seguros/apolices/listar_apolices).
- Cancelar um pedido **antes** da emissão desfaz a venda inteira; cancelar **depois** da emissão dispara o cancelamento de cada apólice individualmente ([Cancelar pedido](/documentation/seguros/pedidos/cancelar_pedido)).

## Endpoints

| Endpoint | Descrição |
|---|---|
| [`POST /v1/insurance/order`](/documentation/seguros/pedidos/criar_pedido) | Cria e submete o pedido. |
| [`GET /v1/insurance/order`](/documentation/seguros/pedidos/listar_pedidos) | Lista os pedidos da sua integração. |
| [`GET /v1/insurance/orders/{order_key}`](/documentation/seguros/pedidos/consultar_pedido) | Detalha um pedido. |
| [`POST /v1/insurance/orders/{order_key}/cancel`](/documentation/seguros/pedidos/cancelar_pedido) | Cancela um pedido (pré ou pós-emissão). |

:::caution Rota de coleção no singular
A rota de coleção é `/v1/insurance/order` (singular) e carrega tanto a criação (`POST`) quanto a listagem (`GET`). As rotas endereçadas por chave usam o plural: `/v1/insurance/orders/{order_key}`.
:::

---

# Listar pedidos

URL: /documentation/seguros/pedidos/listar_pedidos

Retorna a lista paginada dos pedidos da sua integração, do mais recente para o mais antigo.

## Request

ENDPOINT /v1/insurance/order
MÉTODO GET

### Query params

| Parâmetro | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `page` | integer | opcional | Número da página. Padrão: `1`. |
| `page_size` | integer | opcional | Registros por página. Padrão: `50`. Máximo: `200`. |
| `status` | string | opcional | Filtra pelo status do pedido (ex.: `awaiting_payment`). |
| `customer_document_number` | string | opcional | Filtra pelo documento do segurado. |
| `request_control_key` | string | opcional | Filtra pela sua chave de idempotência. |
| `distribution_type` | string | opcional | Filtra pelo modelo de distribuição (ex.: `direct`). |
| `created_from` | string | opcional | Data/hora mínima de criação (ISO 8601). |
| `created_to` | string | opcional | Data/hora máxima de criação (ISO 8601). |

```python title="Exemplo de chamada"
GET /v1/insurance/order?page=1&page_size=50&status=awaiting_payment&created_from=2026-07-01T00:00:00Z
```

## Response

STATUS 200

```json title="Response Body"
{
  "items": [
    {
      "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
      "status": "awaiting_payment",
      "product_keys": ["9d1f8c7a-3b21-4e60-8a2f-1c5d7e9b0a44"],
      "customer_document_number": "96969879003",
      "total_order_amount": 617.28,
      "created_at": "2026-07-14T12:00:00Z"
    }
  ],
  "page": 1,
  "page_size": 50,
  "total": 137
}
```

### Atributos da resposta

| Campo | Tipo | Descrição |
|---|---|---|
| `items` | array | Lista de resumos de pedido. |
| `page` | integer | Página atual. |
| `page_size` | integer | Tamanho da página solicitado. |
| `total` | integer | Total de pedidos que atendem aos filtros. |

#### Objeto em `items`

| Campo | Tipo | Descrição |
|---|---|---|
| `order_key` | string | Chave única do pedido. |
| `status` | string | Status atual. Veja o [ciclo de vida](/documentation/seguros/pedidos/inicio). |
| `product_keys` | array | Chaves dos produtos vendidos no pedido. |
| `customer_document_number` | string | Documento do segurado. |
| `total_order_amount` | number | Valor total do pedido (soma dos prêmios brutos dos produtos). |
| `created_at` | string | Data/hora da submissão. |

## Possíveis erros

Todo erro (`non-2xx`) retorna o corpo padrão `{ "title", "description", "translation", "code" }` — trate programaticamente **apenas** o campo `code`.

| Status | Código | Descrição |
|---|---|---|
| `400` | `QIT000010` | Parâmetro de paginação inválido. |
| `400` | `QIT000001` | Parâmetro de filtro malformado. |
| `401` / `403` | — | Falha de autenticação ou autorização. |
| `429` | — | Limite de requisições excedido — repita com backoff. |
| `500` / `503` | `QIT000500` | Erro interno ou serviço indisponível — seguro repetir a chamada. |

---

# Webhooks do Pedido

URL: /documentation/seguros/pedidos/webhooks

A cada transição de status do pedido, a QI Tech envia um webhook do tipo `insurance.order.status_changed` para a URL de callback configurada. O evento identifica o pedido pelo `order_key` e carrega o novo status.

:::info Webhooks são notificações, não a fonte da verdade
A entrega dos webhooks é do tipo *best-effort*. Não dependa exclusivamente deles: um evento perdido é sempre recuperável consultando o pedido em [`GET /v1/insurance/order`](/documentation/seguros/pedidos/listar_pedidos).
:::

:::info Configuração de webhooks
Para receber webhooks é necessário ter uma URL de callback configurada. Veja [Autenticação — Recebimento de webhooks](/documentation/seguros/introducao/autenticacao#recebimento-de-webhooks).
:::

## Estrutura do webhook

| Campo | Tipo | Descrição |
|---|---|---|
| `webhook_type` | string | Sempre `insurance.order.status_changed`. |
| `webhook_datetime` | string | Data e hora do evento no formato ISO 8601. |
| `data` | object | Dados do evento. Veja tabela abaixo. |

#### Atributos de `data`

| Campo | Tipo | Descrição |
|---|---|---|
| `order_key` | string | Chave do pedido. |
| `status` | string | Novo status do pedido. |
| `customer_document_number` | string | Documento do segurado. |
| `reason` | array / string | Motivo, quando aplicável: a lista de `decline_reasons` em `rejected`, o motivo informado em `cancelled`. `null` nos demais casos — a chave está sempre presente. |
| `product_count` | integer | Quantidade de produtos do pedido. No evento de `emitted`, é o número de apólices que serão emitidas. |

```json title="Estrutura padrão do webhook"
{
  "webhook_type": "insurance.order.status_changed",
  "webhook_datetime": "2026-07-16T14:03:22Z",
  "data": {
    "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
    "status": "status",
    "customer_document_number": "96969879003",
    "reason": null
  }
}
```

## Eventos por status

### Pedido criado

STATUS awaiting_payment

Enviado quando a submissão é concluída com sucesso. O pedido aguarda o pagamento da primeira parcela pelo `payment_artifact` Pix retornado na [criação do pedido](/documentation/seguros/pedidos/criar_pedido).

---

### Pedido recusado

STATUS rejected

Enviado quando o pedido nasce recusado na submissão porque a precificação recusou ao menos uma linha. O campo `reason` traz a lista de `decline_reasons`.

---

### Pedido emitido

STATUS emitted

Enviado quando o pagamento é confirmado e a emissão das apólices é disparada. O campo `product_count` indica quantas apólices serão criadas — **o evento não carrega as chaves das apólices**, porque elas são emitidas de forma assíncrona logo em seguida. Descubra-as com [`GET /v1/insurance/policies?order_key=`](/documentation/seguros/apolices/listar_apolices) ou aguarde os [webhooks de apólice](/documentation/seguros/apolices/webhooks).

```json title="Webhook Body"
{
  "webhook_type": "insurance.order.status_changed",
  "webhook_datetime": "2026-07-16T14:03:22Z",
  "data": {
    "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
    "status": "emitted",
    "customer_document_number": "96969879003",
    "reason": null,
    "product_count": 2
  }
}
```

---

### Pedido expirado

STATUS expired

Enviado quando o prazo de pagamento (`expires_at`) vence sem confirmação. A expiração nunca desfaz um pagamento válido: se o pagamento for confirmado antes do processamento da expiração, o pedido é emitido normalmente e este evento não ocorre.

---

### Pedido cancelado

STATUS cancelled

Enviado quando o [cancelamento pré-emissão](/documentation/seguros/pedidos/cancelar_pedido) é concluído. O campo `reason` traz o motivo informado no cancelamento, quando houver. O cancelamento **pós-emissão** não gera este evento — o pedido permanece `emitted` e o acompanhamento é pelos [webhooks de apólice](/documentation/seguros/apolices/webhooks).

:::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.
:::

---

# 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)
:::

Os eventos de pedido e de apólice são notificados por `POST` na URL de callback configurada para a sua integração.

## Como configurar

No Insurance-as-a-Service a URL de recebimento das notificações **não está disponível pelo portal QI Tech**. Ela é configurada pelo time de Integração da QI Tech.

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

- A **URL de callback**, por ambiente (sandbox e produção). A URL deve ser HTTPS e aceitar `POST`.
- Os **headers** que você precisa que a QI Tech envie nas notificações, se houver — por exemplo um header de autenticação do seu lado.

Avise-nos com antecedência quando a URL mudar: enquanto a alteração não for registrada, as notificações continuam sendo enviadas para o endereço anterior.

## Formato das notificações

Os webhooks da QI Tech seguem uma estrutura padrão:

```json
{
  "webhook_type": "<tipo_do_evento>",
  "webhook_datetime": "2026-07-16T14:03:22Z",
  "data": {}
}
```

| Campo              | Tipo   | Descrição                                   |
| ------------------ | ------ | ------------------------------------------- |
| `webhook_type`     | string | Identificador do tipo de evento.            |
| `webhook_datetime` | string | Data e hora do evento no formato ISO 8601.  |
| `data`             | object | Dados específicos do evento.                |

Os eventos desta linha de produto estão descritos em [Webhooks de Pedido](/documentation/seguros/pedidos/webhooks) e [Webhooks de Apólice](/documentation/seguros/apolices/webhooks).

As notificações são enviadas com headers assinados. Valide a assinatura antes de processar o conteúdo — veja [Validação de Webhooks](/documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_webhook_v2).

:::danger Atenção!
Os webhooks da QI Tech não devem ser mapeados de forma restrita.
Campos adicionais podem ser incluídos aos payloads dos webhooks retornados em nossas APIs.
:::

:::info Informação
O timeout para resposta dos nossos webhooks é de 10 segundos.
:::

---

# 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:

```
-----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.

```
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

Como parte da assinatura das requisições e das respostas, é necessário que você forneça a sua chave pública a nós e que retornemos uma chave pública para você — assim a leitura das mensagens pode ser feita nas duas pontas da comunicação. Além disso, fornecemos uma chave única do tipo UUID (`API-CLIENT-KEY`) que representa a sua integração via API dentro do nosso sistema.

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`) e a **chave pública da QI Tech**.

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

## 1. Introdução e Preparação

### Visão Geral e Importância
Esta seção aborda como a QI Tech envia webhooks com headers assinados, destacando a importância de descriptografar e validar esses headers para garantir segurança nas comunicações.

### Formato das Requisições
As requisições de webhook serão enviadas para a [URL configurada para recebimento dos webhooks](/documentation/seguros/primeiros_passos/seguros_configurando_webhooks). Elas possuem um formato específico de headers e body, detalhado a seguir.

ENDPOINT URL configurada para recebimento dos webhooks
MÉTODO POST

Request Headers

```json
{
    "AUTHORIZATION": "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkX21kNSI6IjRhNjAzZjBmMGU3ZGRkZTlkYTJhMGFkM2QzNDFmNzRiIiwidGltZXN0YW1wIjoiMjAyMy0wNi0zMFQxODo1MjoyNy44ODU3MzFaIiwibWV0aG9kIjoiUE9TVCIsInVyaSI6Ii90ZXN0In0.AcNiJqXDdVmlXSbPI6bH41n0KXz9JwVVMgo4Ivqsq5UZjM2WBOTWw3aAvIMAAhjK5OdrURD4cX3dbbnRgzxspUckANRt0hVHRKSkhROHBfZxuTXVfv8oYzwghwiO2MatPBsroC9Vxbh-DEVQJIBigtN9_D5bg8p2-mlVvoxou2I-EwZs",
    "API-CLIENT-KEY": "20d6a816-9d21-4e29-bbe5-2ffb3baacfe9"
}
```

Request Body

```json
{
    "body_sample": "Exemplo de webhook"
}
```

## 2. Configuração e Descriptografia

### Importar bibliotecas

Antes de começar a descriptografia e validação dos webhooks, é essencial importar as bibliotecas necessárias em sua linguagem de programação preferida. Estas bibliotecas facilitarão o trabalho com JWTs, criptografia e outros aspectos relacionados.

**Python**

```python
import json
from datetime import datetime, timedelta
from hashlib import md5
from jose import jwt
```

**PHP**

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

**Node.js**

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

**Java**

```java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.io.pem.PemReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
```

**C#**

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

### Definir variáveis

Defina as variáveis necessárias para manipular os headers e o corpo do webhook. Isso inclui a chave pública fornecida pela QI Tech, utilizada para descriptografar e validar o webhook.

**Python**

```python
headers = {
    "AUTHORIZATION": "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkX21kNSI6IjRhNjAzZjBmMGU3ZGRkZTlkYTJhMGFkM2QzNDFmNzRiIiwidGltZXN0YW1wIjoiMjAyMy0wNi0zMFQxODo1MjoyNy44ODU3MzFaIiwibWV0aG9kIjoiUE9TVCIsInVyaSI6Ii90ZXN0In0.AcNiJqXDdVmlXSbPI6bH41n0KXz9JwVVMgo4Ivqsq5UZjM2WBOTWw3aAvIMAAhjK5OdrURD4cX3dbbnRgzxspUckANRt0hVHRKSkhROHBfZxuTXVfv8oYzwghwiO2MatPBsroC9Vxbh-DEVQJIBigtN9_D5bg8p2-mlVvoxou2I-EwZs",
    "API-CLIENT-KEY": "20d6a816-9d21-4e29-bbe5-2ffb3baacfe9",
}
body = {"body_sample": "Exemplo de webhook"}
authorization = headers.get("AUTHORIZATION")
```

**PHP**

```php
$headers = [
    "AUTHORIZATION" => "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkX21kNSI6IjRhNjAzZjBmMGU3ZGRkZTlkYTJhMGFkM2QzNDFmNzRiIiwidGltZXN0YW1wIjoiMjAyMy0wNi0zMFQxODo1MjoyNy44ODU3MzFaIiwibWV0aG9kIjoiUE9TVCIsInVyaSI6Ii90ZXN0In0.AcNiJqXDdVmlXSbPI6bH41n0KXz9JwVVMgo4Ivqsq5UZjM2WBOTWw3aAvIMAAhjK5OdrURD4cX3dbbnRgzxspUckANRt0hVHRKSkhROHBfZxuTXVfv8oYzwghwiO2MatPBsroC9Vxbh-DEVQJIBigtN9_D5bg8p2-mlVvoxou2I-EwZs",
    "API-CLIENT-KEY" => "20d6a816-9d21-4e29-bbe5-2ffb3baacfe9",
];
$body = ["body_sample" => "Exemplo de webhook"];
$authorization = $headers["AUTHORIZATION"];
```

**Node.js**

```js
const headers = {
  AUTHORIZATION: 'eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkX21kNSI6IjRhNjAzZjBmMGU3ZGRkZTlkYTJhMGFkM2QzNDFmNzRiIiwidGltZXN0YW1wIjoiMjAyMy0wNi0zMFQxODo1MjoyNy44ODU3MzFaIiwibWV0aG9kIjoiUE9TVCIsInVyaSI6Ii90ZXN0In0.AcNiJqXDdVmlXSbPI6bH41n0KXz9JwVVMgo4Ivqsq5UZjM2WBOTWw3aAvIMAAhjK5OdrURD4cX3dbbnRgzxspUckANRt0hVHRKSkhROHBfZxuTXVfv8oYzwghwiO2MatPBsroC9Vxbh-DEVQJIBigtN9_D5bg8p2-mlVvoxou2I-EwZs',
  'API-CLIENT-KEY': '20d6a816-9d21-4e29-bbe5-2ffb3baacfe9'
};
const body = { body_sample: 'Exemplo de webhook' };
```

**Java**

```java
String authorization = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkX21kNSI6IjRhNjAzZjBmMGU3ZGRkZTlkYTJhMGFkM2QzNDFmNzRiIiwidGltZXN0YW1wIjoiMjAyMy0wNi0zMFQxODo1MjoyNy44ODU3MzFaIiwibWV0aG9kIjoiUE9TVCIsInVyaSI6Ii90ZXN0In0.AcNiJqXDdVmlXSbPI6bH41n0KXz9JwVVMgo4Ivqsq5UZjM2WBOTWw3aAvIMAAhjK5OdrURD4cX3dbbnRgzxspUckANRt0hVHRKSkhROHBfZxuTXVfv8oYzwghwiO2MatPBsroC9Vxbh-DEVQJIBigtN9_D5bg8p2-mlVvoxou2I-EwZs";
```

**C#**

```c#
var headers = new Dictionary<string, string>()
{
    { "AUTHORIZATION", "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkX21kNSI6IjRhNjAzZjBmMGU3ZGRkZTlkYTJhMGFkM2QzNDFmNzRiIiwidGltZXN0YW1wIjoiMjAyMy0wNi0zMFQxODo1MjoyNy44ODU3MzFaIiwibWV0aG9kIjoiUE9TVCIsInVyaSI6Ii90ZXN0In0.AcNiJqXDdVmlXSbPI6bH41n0KXz9JwVVMgo4Ivqsq5UZjM2WBOTWw3aAvIMAAhjK5OdrURD4cX3dbbnRgzxspUckANRt0hVHRKSkhROHBfZxuTXVfv8oYzwghwiO2MatPBsroC9Vxbh-DEVQJIBigtN9_D5bg8p2-mlVvoxou2I-EwZs" },
    { "API-CLIENT-KEY", "20d6a816-9d21-4e29-bbe5-2ffb3baacfe9" }
};

var body = new Dictionary<string, string>()
{
    { "body_sample", "Exemplo de webhook" }
};
```

### 2. Inserção de Dados de Criptografia e Realização da Descriptografia
Inserimos a chave pública fornecida pela QI Tech e realizamos a descriptografia do header do webhook. Essa chave é crucial para a descriptografia dos headers do webhook.

**Python**

```python
qi_public_key = """-----BEGIN PUBLIC KEY-----
{QI_PUBLIC_KEY}
-----END PUBLIC KEY-----"""
```

**PHP**

```php
$qiPublicKey = "-----BEGIN PUBLIC KEY-----
{QI_PUBLIC_KEY}
-----END PUBLIC KEY-----";
```

**Node.js**

```js
const qiPublicKey = `-----BEGIN PUBLIC KEY-----
{QI_PUBLIC_KEY}
-----END PUBLIC KEY-----`;
```

**Java**

```java
String publicKeyStr = "{QI_PUBLIC_KEY}";
```

**C#**

```c#
var authorization = headers["AUTHORIZATION"];

var qiPublicKey = @"-----BEGIN PUBLIC KEY-----
{QI_PUBLIC_KEY}
-----END PUBLIC KEY-----";
```

### Realizar descriptografia do header

O processo de descriptografia é essencial para verificar a autenticidade e integridade do webhook recebido.

**Python**

```python
try:
    decoded_header = jwt.decode(token=authorization, key=qi_public_key)
except:
    raise Exception("Decodification failed.")
```

**PHP**

```php
$algorithmManager = new AlgorithmManager([new ES512()]);
$jwsVerifier = new JWSVerifier($algorithmManager);
$publicKey = JWKFactory::createFromKey($qiPublicKey, null, ['use' => 'sig']);
$serializerManager = new JWSSerializerManager([new CompactSerializer]);
$jws = $serializerManager->unserialize($authorization);
$decodedHeader = json_decode($jws->getPayload(), true);
```

**Node.js**

```js
const decodedHeader = jwt.verify(authorization, qiPublicKey);
```

**Java**

```java
private static Claims validate(final String encodedBody, String publicKeyStr){
    try {
        Security.addProvider(new BouncyCastleProvider());

        final String pbKey = new String(Base64.getDecoder().decode(publicKeyStr));
        Reader rdr = new StringReader(pbKey);
        PemReader pemParser = new PemReader(rdr);

        X509EncodedKeySpec spec = new X509EncodedKeySpec(pemParser.readPemObject().getContent());
        KeyFactory kf = KeyFactory.getInstance("EC");

        PublicKey publicKey = kf.generatePublic(spec);
        return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(encodedBody).getBody();
    }  catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {
        throw new IllegalStateException(e);
    }
}
```

**C#**

```c#
var key = ECDsa.Create();
key.ImportFromPem(qiPublicKey);
var decodedHeader = JWT.Decode<IDictionary<string, string>>(authorization, key);
```

## 3. Validação e Conclusão

### Realização de Validações

Após descriptografar o header, é importante realizar várias validações para garantir que o webhook é válido e seguro.

**Python**

```python
assert decoded_header.get("method") == "POST"
assert decoded_header.get("uri") == "/client_webhook_endpoint"
assert (
    decoded_header.get("payload_md5")
    == md5(json.dumps(body).encode()).hexdigest()
)
assert (
    (datetime.now() - timedelta(minutes=5))
    < datetime.strptime(decoded_header.get("timestamp"), "%Y-%m-%dT%H:%M:%S.%fZ")
    < (datetime.now() + timedelta(minutes=5))
)
```

**PHP**

```php
$method = $decodedHeader["method"];
$uri = $decodedHeader["uri"];
$payloadMd5 = $decodedHeader["payload_md5"];
$timestamp = $decodedHeader["timestamp"];

assert($method === "POST");
assert($uri === "/client_webhook_endpoint");
assert($payloadMd5 === md5(json_encode($body, JSON_UNESCAPED_SLASHES)));
assert(
    (new DateTime("now", new DateTimeZone("UTC")))->sub(new DateInterval("PT5M")) < DateTime::createFromFormat("Y-m-d\TH:i:s.u\Z", $timestamp) &&
    DateTime::createFromFormat("Y-m-d\TH:i:s.u\Z", $timestamp) < (new DateTime("now", new DateTimeZone("UTC")))->add(new DateInterval("PT5M"))
);
```

**Node.js**

```js
if (decodedHeader.method !== 'POST') {
    throw new Error('Invalid method');
  }
  
  if (decodedHeader.uri !== '/client_webhook_endpoint') {
    throw new Error('Invalid URI');
  }
  
  const payloadMd5 = crypto
    .createHash('md5')
    .update(JSON.stringify(body))
    .digest('hex');
    
  if (decodedHeader.payload_md5 !== payloadMd5) {
    throw new Error('Invalid payload MD5');
  }
  
  const timestamp = new Date(decodedHeader.timestamp);
  const currentDateTime = new Date();
  
  const fiveMinutesAgo = new Date(currentDateTime.getTime() - 5 * 60000);
  const fiveMinutesAhead = new Date(currentDateTime.getTime() + 5 * 60000);
  
  if (!(timestamp > fiveMinutesAgo && timestamp < fiveMinutesAhead)) {
    throw new Error('Invalid timestamp');
  }
```

**Java**

```java
Claims result = validate(authorization, publicKeyStr);
System.out.println(result);
System.out.println(result.get("method").equals("POST"));
System.out.println(result.get("uri").equals("/test"));
```

**C#**

```c#
var method = decodedHeader["method"];
var uri = decodedHeader["uri"];
var payloadMd5 = decodedHeader["payload_md5"];
var timestamp = DateTime.Parse(decodedHeader["timestamp"]);
timestamp = timestamp.ToUniversalTime();
var bodyJson = JsonConvert.SerializeObject(body, new JsonSerializerSettings
{
    NullValueHandling = NullValueHandling.Ignore,
    Formatting = Formatting.None
});

using (var md5Hash = MD5.Create())
{
    var calculatedMd5 = GetMd5Hash(md5Hash, bodyJson);
    if (payloadMd5 != calculatedMd5)
    {
        throw new Exception("Payload MD5 verification failed.");
    }
}

var currentTime = datetime.now;
var validTimeStart = currentTime.AddMinutes(-5);
var validTimeEnd = currentTime.AddMinutes(5);
if (timestamp < validTimeStart || timestamp > validTimeEnd)
{
    throw new Exception("Timestamp verification failed.");
}

...

static string GetMd5Hash(MD5 md5Hash, string input)
{
    byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input));

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

    return builder.ToString();
}
```