# 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
31 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)
- Simular pagamento (/documentation/seguros/pedidos/simular_pagamento)
- 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. 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.

O cancelamento vale **no momento da chamada**: a resposta já traz a apólice como `canceled`, a cobrança das parcelas seguintes é interrompida e o [webhook de apólice cancelada](/documentation/seguros/apolices/webhooks#apolice-cancelada) é enviado. A comunicação com a seguradora acontece em seguida, de forma assíncrona, e não desfaz o cancelamento.

## Devolução de prêmio

A devolução, quando devida, é calculada sobre o que já foi pago, a partir do momento da solicitação:

| Momento do cancelamento | Devolução |
|---|---|
| Até 7 dias corridos da contratação — direito de arrependimento | **Integral** do que foi pago. |
| Após 7 dias | **Proporcional**: a parte do que foi pago que excede o período de cobertura já decorrido. |

## 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, de 1 a 255 caracteres, registrado na trilha de eventos e enviado no webhook de cancelamento. Sem motivo informado, é registrado `requester_requested`. O corpo inteiro é opcional. |

## Response

STATUS 200

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

### Semântica por status

| Status atual da apólice | Resultado |
|---|---|
| `active` | `200` — a apólice é cancelada. |
| `active`, com a vigência já encerrada | `409` `POL000011` — a cobertura já terminou. |
| `canceled` | `409` `POL000012` — a apólice já foi cancelada. |
| `finished` | `409` `POL000011` — a vigência já terminou. |
| outra integração / inexistente | `404` `POL000001` |

## 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` | `POL000011` | A vigência da cobertura já terminou. |
| `409` | `POL000012` | A apólice já foi cancelada. |

---

# 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 logo após a emissão do pedido e passa a ser a fonte da verdade sobre a cobertura: número da apólice, vigência, coberturas efetivas, prêmio decomposto e trilha de eventos.

## Ciclo de vida da apólice

![Fluxo de status da apólice, da criação ao cancelamento ou ao fim da vigência](/img/diagrams/seguros-apolices-inicio.svg)

_Como ler o diagrama: **azul** = apólice em vigor · **vermelho** = cancelada (gera webhook) · **verde** = encerramento natural (não gera webhook)._

| Status | Significado |
|---|---|
| `active` | A apólice está em vigor desde a criação. A confirmação da emissão pela seguradora não muda o status: ela aparece como o `external_policy_number` preenchido e gera o [webhook](/documentation/seguros/apolices/webhooks) `issued`. Até lá, o `external_policy_number` é `null`. |
| `canceled` | A apólice foi cancelada — pelo [cancelamento da apólice](/documentation/seguros/apolices/cancelar_apolice), pelo [cancelamento do pedido](/documentation/seguros/pedidos/cancelar_pedido) depois da emissão ou pela seguradora. O status muda no momento do cancelamento e gera o webhook `canceled`. |
| `finished` | A vigência chegou ao fim (`term.end_date`). Status terminal, sem webhook — o fim de vigência é conhecido desde a emissão. |

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

A devolução, quando devida, é calculada sobre o que já foi pago, a partir do momento da solicitação do cancelamento:

- **Até 7 dias corridos da contratação** — direito de arrependimento: devolução **integral** do que foi pago.
- **Após 7 dias**: devolução **proporcional** — a parte do que foi pago que excede o período de cobertura já decorrido.

## 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` (apólice cancelada). O encerramento natural de vigência (`finished`) **não** gera webhook — ele é conhecido desde a emissão pelo `term.end_date`.

Cada apólice é notificada de forma independente: um pedido com N produtos gera N apólices e N eventos de cada tipo, possivelmente em momentos diferentes.

:::info Configuração e validação

Para receber webhooks é necessário ter uma URL de webhook configurada, e cada webhook deve ter a assinatura validada. Veja [Configurando webhooks](/documentation/seguros/primeiros_passos/seguros_configurando_webhooks) e [Validação de webhooks](/documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_webhook_v2).

:::

## Estrutura do webhook

| Campo | Tipo | Descrição |
|---|---|---|
| `webhook_type` | string | Sempre `insurance.policy.status_changed`. |
| `status` | string | Novo status da apólice: `issued` ou `canceled`. |
| `key` | string | Chave da apólice (`policy_key`). |
| `event_datetime` | string | Data e hora do evento, em UTC, 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. |
| `requester_key` | string | Chave da sua integração como distribuidor. |
| `status` | string | Novo status da apólice — o mesmo do campo `status` externo. |
| `external_policy_number` | string | Número da apólice na seguradora. |
| `reason` | string / null | Motivo do cancelamento no evento de `canceled`; `null` no de `issued`. |
| `sequence` | integer | Ordem do evento dentro da apólice: um valor maior é sempre um evento posterior. A sequência não é contínua — o primeiro webhook de uma apólice pode chegar com um valor maior que `1`. |
| `schema_version` | integer | Versão da estrutura de `data`. Atualmente `1`. |

:::caution Atenção

O status de cancelamento da apólice é `canceled`, com um **l**. O do pedido é `cancelled`, com dois. Trate os dois valores separadamente.

:::

## Eventos por status

### Apólice emitida

STATUS issued

Enviado quando a seguradora confirma a emissão. A partir deste evento a cobertura está formalmente registrada na seguradora.

```json title="Webhook Body"
{
  "status": "issued",
  "webhook_type": "insurance.policy.status_changed",
  "key": "0b6e7c1a-9a4e-4c1e-b1d4-2f5a8c9e0d31",
  "event_datetime": "2026-10-05T20:41:02Z",
  "data": {
    "policy_key": "0b6e7c1a-9a4e-4c1e-b1d4-2f5a8c9e0d31",
    "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
    "requester_key": "3f1c9a2e-4b7d-4e0a-9c55-2d8e7f6a1b30",
    "status": "issued",
    "external_policy_number": "QIS000000000123",
    "reason": null,
    "sequence": 1,
    "schema_version": 1
  }
}
```

---

### Apólice cancelada

STATUS canceled

Enviado quando a apólice é cancelada — pelo [cancelamento da apólice](/documentation/seguros/apolices/cancelar_apolice) ou do [pedido depois da emissão](/documentation/seguros/pedidos/cancelar_pedido), no momento do cancelamento, ou pela seguradora. O campo `reason` traz o motivo informado no cancelamento, ou `requester_requested` se nenhum foi informado; num cancelamento feito pela seguradora, ele vem como `provider_initiated`.

```json title="Webhook Body"
{
  "status": "canceled",
  "webhook_type": "insurance.policy.status_changed",
  "key": "0b6e7c1a-9a4e-4c1e-b1d4-2f5a8c9e0d31",
  "event_datetime": "2026-10-05T21:18:33Z",
  "data": {
    "policy_key": "0b6e7c1a-9a4e-4c1e-b1d4-2f5a8c9e0d31",
    "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
    "requester_key": "3f1c9a2e-4b7d-4e0a-9c55-2d8e7f6a1b30",
    "status": "canceled",
    "external_policy_number": "QIS000000000123",
    "reason": "Cliente solicitou o cancelamento",
    "sequence": 3,
    "schema_version": 1
  }
}
```

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

---

# Introdução

URL: /documentation/seguros/introducao/inicio

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

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

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

## Visão geral da jornada

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

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

## Ambientes (Hosts)

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

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

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

## Convenções da API

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

## Para começar

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

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

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

---

# 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 é cancelada como no [cancelamento de apólice](/documentation/seguros/apolices/cancelar_apolice) e notificada pelo seu próprio [webhook de apólice](/documentation/seguros/apolices/webhooks).

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

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

---

# Simular pagamento

URL: /documentation/seguros/pedidos/simular_pagamento

Simula o pagamento da cobrança em aberto de um pedido, como se o segurado tivesse autorizado o Pix Automático e pago a primeira parcela. Use-o para testar a jornada completa — emissão do pedido, criação das apólices e os [webhooks](/documentation/seguros/pedidos/webhooks) — sem um pagamento real.

:::info Disponível apenas no sandbox

Este endpoint não existe em produção. Lá, o pedido é emitido quando o segurado paga a primeira parcela pelo `payment_artifact` Pix retornado na [criação do pedido](/documentation/seguros/pedidos/criar_pedido).

:::

O pagamento é **assíncrono**: a chamada responde assim que o Pix é simulado, e o pedido passa a `emitted` alguns segundos depois, quando o pagamento é conciliado. Acompanhe pelo [webhook de pedido emitido](/documentation/seguros/pedidos/webhooks) ou pela [consulta do pedido](/documentation/seguros/pedidos/consultar_pedido) — uma consulta feita logo após a chamada ainda mostra `awaiting_payment`.

## Request

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

### Path params

| Parâmetro | Tipo | Obrigatoriedade | Descrição |
|---|---|---|---|
| `order_key` | string | obrigatório | Chave do pedido, que deve estar em `awaiting_payment`. |

A requisição não tem corpo.

## Response

STATUS 202

```json title="Response Body"
{
  "insurance_order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
  "recurrence_status": "pending_confirmation",
  "recurrence_approval_requested": true,
  "charge_key": "9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f",
  "incoming_pix": {
    "receiver_conciliation_id": "a3f1c9e27b4d4e0a9c552d8e7f6a1b30",
    "amount": 613.62,
    "end_to_end_id": "E00000000202610051559A1B2C3D4E5F",
    "pix_transfer_key": "7b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e"
  }
}
```

| Campo | Tipo | Descrição |
|---|---|---|
| `insurance_order_key` | string | Chave do pedido pago — o mesmo `order_key` da chamada. |
| `recurrence_status` | string | Status da autorização do Pix Automático **antes** da simulação. |
| `recurrence_approval_requested` | boolean | `true` quando a simulação também aprovou a autorização, que ainda estava pendente. |
| `charge_key` | string | Chave da cobrança paga — a mais antiga em aberto do pedido. |
| `incoming_pix` | object | O Pix simulado: identificador de conciliação, valor pago, `end_to_end_id` e chave da transferência. |

## 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` | `CAP000003` | Pedido inexistente ou pertencente a outra integração. |
| `404` | `CAP000008` | O pedido não tem uma autorização de Pix Automático ativa — por exemplo, um pedido cancelado ou expirado. |
| `422` | `CAP000021` | O pedido não tem cobrança em aberto — por exemplo, um pedido já pago. |
| `422` | `CAP000022` | A autorização do pedido não tem o Pix de ativação a ser simulado. |

---

# Webhooks do pedido

URL: /documentation/seguros/pedidos/webhooks

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

:::info Configuração e validação

Para receber webhooks é necessário ter uma URL de webhook configurada, e cada webhook deve ter a assinatura validada. Veja [Configurando webhooks](/documentation/seguros/primeiros_passos/seguros_configurando_webhooks) e [Validação de webhooks](/documentation/seguros/primeiros_passos/teste_de_autenticacao/seguros_webhook_v2).

:::

## Estrutura do webhook

| Campo | Tipo | Descrição |
|---|---|---|
| `webhook_type` | string | Sempre `insurance.order.status_changed`. |
| `status` | string | Novo status do pedido. |
| `key` | string | Chave do pedido (`order_key`). |
| `event_datetime` | string | Data e hora do evento, em UTC, 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. |
| `requester_key` | string | Chave da sua integração como distribuidor. |
| `status` | string | Novo status do pedido — o mesmo do campo `status` externo. |
| `customer_document_number` | string | Documento do segurado. |
| `reason` | array / string / null | Motivo da mudança: a lista de `decline_reasons` em `rejected`, o motivo informado em `cancelled` e `null` nos demais. 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. |
| `sequence` | integer | Ordem do evento dentro do pedido: o evento de criação é `1` e cada mudança seguinte tem um valor maior. Use-o para ordenar webhooks que chegarem fora de ordem. |
| `schema_version` | integer | Versão da estrutura de `data`. Atualmente `1`. |

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

## Eventos por status

### Pedido criado

STATUS awaiting_payment

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

---

### Pedido recusado

STATUS rejected

Enviado quando o pedido nasce recusado na criação porque a precificação recusou ao menos uma linha. O campo `reason` traz a lista de `decline_reasons`, uma entrada por falha, com os [mesmos códigos da cotação](/documentation/seguros/cotacao/criar_cotacao#motivos-de-rejeicao).

```json title="Webhook Body"
{
  "status": "rejected",
  "webhook_type": "insurance.order.status_changed",
  "key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
  "event_datetime": "2026-10-05T16:06:22Z",
  "data": {
    "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
    "requester_key": "3f1c9a2e-4b7d-4e0a-9c55-2d8e7f6a1b30",
    "status": "rejected",
    "customer_document_number": "96969879003",
    "reason": [
      {
        "product_key": "8a6f2c1d-3b4e-4f5a-9b8c-7d6e5f4a3b2c",
        "code": "OUT_OF_OPTION_SPACE",
        "detail": "1f2e3d4c-5b6a-4789-8a7b-6c5d4e3f2a1b: insured_amount: 60000 is outside the offered range 0-50000.0"
      }
    ],
    "product_count": 1,
    "sequence": 1,
    "schema_version": 1
  }
}
```

---

### Pedido emitido

STATUS emitted

Enviado quando o pagamento da primeira parcela é 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 criadas de forma assíncrona logo em seguida. No sandbox, o pagamento pode ser [simulado](/documentation/seguros/pedidos/simular_pagamento). Descubra as apólices 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"
{
  "status": "emitted",
  "webhook_type": "insurance.order.status_changed",
  "key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
  "event_datetime": "2026-10-05T15:59:34Z",
  "data": {
    "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
    "requester_key": "3f1c9a2e-4b7d-4e0a-9c55-2d8e7f6a1b30",
    "status": "emitted",
    "customer_document_number": "96969879003",
    "reason": null,
    "product_count": 1,
    "sequence": 2,
    "schema_version": 1
  }
}
```

---

### Pedido expirado

STATUS expired

Enviado quando o prazo de pagamento (`expires_at`) vence sem confirmação. O vencimento é verificado a cada 15 minutos, então o evento pode chegar até 15 minutos depois do prazo. 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.

```json title="Webhook Body"
{
  "status": "expired",
  "webhook_type": "insurance.order.status_changed",
  "key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
  "event_datetime": "2026-10-13T00:00:12Z",
  "data": {
    "order_key": "5e2b3f60-7c4b-4c8e-9f1a-6d2e8b4a7c90",
    "requester_key": "3f1c9a2e-4b7d-4e0a-9c55-2d8e7f6a1b30",
    "status": "expired",
    "customer_document_number": "96969879003",
    "reason": null,
    "product_count": 1,
    "sequence": 2,
    "schema_version": 1
  }
}
```

---

### Pedido cancelado

STATUS cancelled

Enviado quando o [cancelamento antes da emissão](/documentation/seguros/pedidos/cancelar_pedido) é concluído. O campo `reason` traz o motivo informado no cancelamento, ou `null` se nenhum foi informado.

O cancelamento **depois da emissão** não gera este evento: o pedido permanece `emitted` e cada apólice é cancelada, o que é notificado pelos [webhooks de apólice](/documentation/seguros/apolices/webhooks).

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

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

:::

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

## Eventos disponíveis

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

## Como configurar

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

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

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

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

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

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

## Formato dos webhooks

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

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

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

### Headers

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

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

## Como responder

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

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

## Ordem e duplicidade

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

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

:::danger Atenção!

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

:::

---

# Configurar IP de Integração

URL: /documentation/seguros/primeiros_passos/seguros_configurar_ip_de_integracao

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

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

## Como configurar

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

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

O que é aceito em cada entrada:

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

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

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

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

## Possíveis erros

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

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

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

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

---

# Troca de chaves

URL: /documentation/seguros/primeiros_passos/seguros_troca_de_chaves

## Conferindo o formato da chave pública

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

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

Confira o arquivo antes de nos enviar:

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

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

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

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

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

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

## Envio da chave pública

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

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

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

:::danger Atenção!

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

:::

---

# Possíveis erros

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

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

## Erro no token

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

STATUS 401

Response Body

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

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

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

STATUS 400

Response Body

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

## <strong>API_KEY</strong> incorreta

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

STATUS 404

Response Body

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

## Endpoint não autorizado

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

STATUS 401

Response Body

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

:::caution Atenção!

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

---

# Exemplo completo de teste de autenticação

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

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

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

:::caution Atenção!

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

```
d41d8cd98f00b204e9800998ecf8427e
```

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

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

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

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

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

**Python**

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

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

    if request_body is None:
        request_body = {}

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

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

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

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

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

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

    return signed_header

if __name__ == "__main__":

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

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

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

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

**PHP**

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return $headers;
}

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

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

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

    $response = null;

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

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

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

    }
}
?>
```

**Node.js**

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

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

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

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

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

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

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

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

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

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

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

    let ENDPOINT;
    let url;
    let signed_header;

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

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

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

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

main();
```

**Java**

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

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

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

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

public class QItechApiClient {

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

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

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

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

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

        return signedHeader;
    }

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

        try {

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

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

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

            String endpoint;
            Request request;

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

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

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

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

            } else {
                endpoint = "/test";

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

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

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

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

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

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

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

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

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

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

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

////POM File

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

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

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

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

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

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

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

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

```

**C#**

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

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

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

        string payloadToHash;

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

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

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

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

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

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

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

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

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

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

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

            HttpResponseMessage httpResponse;

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

            httpResponse.EnsureSuccessStatusCode();

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

            return responseContent;
        }
    }
}

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

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

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

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

---

# Teste de autenticação

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

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

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

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

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

**Python**

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

**PHP**

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

**Node.js**

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

**Java**

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

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

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

**C#**

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

### Definir variáveis

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

**Python**

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

**PHP**

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

**Node.js**

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

**Java**

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

**C#**

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

```
  

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

### Inserir dados de criptografia

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

**Python**

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

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

**PHP**

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

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

**Node.js**

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

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

**Java**

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

**C#**

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

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

**Python**

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

**PHP**

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

**Node.js**

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

**Java**

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

**C#**

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

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

**Python**

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

**PHP**

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

**Node.js**

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

**Java**

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

**C#**

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

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

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

**Python**

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

**PHP**

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

**Node.js**

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

**Java**

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

...

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

...

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

**C#**

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

.

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

        StringBuilder builder = new StringBuilder();

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

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

:::caution Atenção!

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

```
d41d8cd98f00b204e9800998ecf8427e
```

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

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

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

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

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

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

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

**Python**

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

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

**PHP**

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

**Node.js**

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

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

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

**Java**

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

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

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

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

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

**C#**

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

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

**Python**

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

**PHP**

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

**Node.js**

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

**Java**

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

**C#**

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

### Realizar criptografia do header

**Python**

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

**PHP**

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

**Node.js**

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

**Java**

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

...

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

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

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

**C#**

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

### Montar header assinado

**Python**

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

**PHP**

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

**Node.js**

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

**Java**

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

**C#**

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

### Construir a URL da solicitação

**Python**

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

**PHP**

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

**Node.js**

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

**Java**

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

**C#**

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

## 3. Realizar Requisição

**Python**

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

**PHP**

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

**Node.js**

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

**Java**

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

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

**C#**

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

---

# Validação de webhooks

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

:::info Veja também

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

:::

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

## O que é assinado

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

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

## Como calcular o `payload_md5`

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

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

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

:::caution Atenção

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

:::

## Exemplo de validação

**Python**

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

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

TOLERANCIA = timedelta(minutes=5)

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

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

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

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

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

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

    return corpo
```

**Node.js**

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

const TOLERANCIA_MS = 5 * 60 * 1000;

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

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

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

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

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

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

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

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

  return corpo;
}
```

## Dados para testar

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

:::info Informação

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

:::

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

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

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

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