# Catálogo de erros

Toda a falha da API de Parceiros Fintra devolve o mesmo envelope:

```json
{
  "error": {
    "code": "…",
    "message": "…",
    "hint": "…",
    "doc_url": "https://developers.fintra.co.mz/erros#…",
    "request_id": "…"
  }
}
```

**Ramifique sempre por `error.code`**, nunca pelo texto de `message`: o código é estável, o texto não.
O `request_id` repete-se no cabeçalho `X-Request-Id`; guarde-o nos seus logs.

Só o `validation_failed` traz um campo a mais (`error.errors`).

Estes 15 códigos são os únicos que a v1 emite. Se receber um `code` que não esteja aqui, é um erro
nosso — envie o `request_id` para `suporte@fintra.co.mz`.

| Código | HTTP | Repetir o pedido ajuda? |
| --- | --- | --- |
| [`invalid_api_key`](#invalid_api_key) | 401 | Não |
| [`api_key_expired`](#api_key_expired) | 401 | Não |
| [`api_key_revoked`](#api_key_revoked) | 401 | Não |
| [`partner_api_disabled`](#partner_api_disabled) | 403 | Não |
| [`browser_use_forbidden`](#browser_use_forbidden) | 403 | Não |
| [`test_mode_read_only`](#test_mode_read_only) | 403 | Não |
| [`insufficient_scope`](#insufficient_scope) | 403 | Não |
| [`resource_not_found`](#resource_not_found) | 404 | Não |
| [`invalid_query_parameter`](#invalid_query_parameter) | 422 | Não |
| [`validation_failed`](#validation_failed) | 422 | Não |
| [`idempotency_key_required`](#idempotency_key_required) | 400 | Não |
| [`idempotency_key_invalid`](#idempotency_key_invalid) | 400 | Não |
| [`idempotency_request_in_flight`](#idempotency_request_in_flight) | 409 | **Sim**, com a MESMA chave |
| [`idempotency_key_reuse`](#idempotency_key_reuse) | 409 | Não |
| [`rate_limit_exceeded`](#rate_limit_exceeded) | 429 | **Sim**, após `Retry-After` |

---

## 401 — a chave

### invalid_api_key

**HTTP 401.** A chave não veio, veio noutro formato, ou não é conhecida.

Causas concretas: falta o cabeçalho `Authorization`; o cabeçalho não começa por `Bearer ` (com
espaço); a chave foi copiada com espaços ou quebra de linha; a chave não existe.

**O que fazer:** confirme o cabeçalho exacto — `Authorization: Bearer fintra_sk_live_...`. Se estiver
correcto e continuar a falhar, a chave não é válida: peça uma nova ao suporte Fintra. **Não repita**
o pedido em ciclo — pedidos sem chave válida têm um tecto de 30/minuto por IP e passará a apanhar 429.

```json
{"error":{"code":"invalid_api_key","message":"Chave de API inválida ou em falta.","hint":"Envie o cabeçalho \"Authorization: Bearer fintra_sk_live_...\". A chave é mostrada uma única vez na emissão; se a perdeu, peça uma nova ao suporte Fintra.","doc_url":"https://developers.fintra.co.mz/erros#invalid_api_key","request_id":"…"}}
```

### api_key_expired

**HTTP 401.** A chave existe mas passou da data de validade (máximo 365 dias).

**O que fazer:** peça uma chave nova ao suporte. Repetir não adianta. Se a sua integração é de longo
prazo, ponha um lembrete 30 dias antes da expiração — a API não avisa.

### api_key_revoked

**HTTP 401.** A chave foi desactivada pelo suporte Fintra.

Normalmente isto acontece porque a chave foi exposta, porque a empresa pediu, ou porque foi
substituída. **O que fazer:** pare de a usar e peça uma nova. Se não sabe porque foi revogada,
pergunte antes de pedir outra — pode haver um incidente de segurança por trás.

---

## 403 — a porta está fechada

### partner_api_disabled

**HTTP 403.** A chave é válida, mas a API de parceiros **não está activada nesta empresa**.
Vem desligada por omissão em todas as empresas.

**O que fazer:** peça ao suporte Fintra para a activar nessa empresa. Nada do lado do seu código
resolve isto.

### browser_use_forbidden

**HTTP 403.** O pedido trouxe o cabeçalho `Origin` — sinal de JavaScript a correr num browser.

Este é o erro número um de quem começa, e o mais caro: uma chave dentro de código de browser fica
visível a qualquer visitante da página, e com ela vêm os clientes, as facturas e os valores da empresa.

**O que fazer:** mova a chamada para o seu **servidor**. O browser fala com o seu servidor; o seu
servidor fala com a Fintra. Não tente remover o `Origin` do pedido — se conseguisse, a chave
continuaria publicada na mesma.

### test_mode_read_only

**HTTP 403.** Tentou escrever (`POST`/`PUT`) com uma chave `fintra_sk_test_...`.

Não existe empresa-sandbox: as chaves de teste apontam para os dados reais e por isso só respondem a
`GET`. **O que fazer:** para desenvolver escrita use uma chave `fintra_sk_live_...` — e faça-o numa
empresa de ensaio, porque o que criar é real.

### insufficient_scope

**HTTP 403.** A chave é válida mas não inclui o scope exigido pela rota. O `hint` diz qual falta.

Scopes da v1: `customers:read`, `customers:write`, `products:read`, `products:write`,
`invoices:read`, `invoices:write`, `payments:read`.

**O que fazer:** peça ao suporte uma chave que inclua o scope em falta. Verifique os scopes que tem
com `GET /ping` antes de assumir que a rota está avariada.

---

## 404

### resource_not_found

**HTTP 404.** O id pedido não é alcançável por esta chave.

**Um só código para três situações**, de propósito: o id não existe, o id está mal formado
(`/customers/abc`), ou o id **pertence a outra empresa**. Se a resposta distinguisse os casos, a API
estava a confirmar a existência de dados de terceiros.

**O que fazer:** liste o recurso (`GET /customers`, `GET /products`, …) e use um id de lá.
Não conclua que o registo não existe — conclua que a sua chave não lhe chega.

Também é o que recebe ao criar uma factura para um `customer_id` que não é da empresa da chave.

---

## 422 — os dados

### invalid_query_parameter

**HTTP 422.** Um parâmetro da *query string* não é aceite. A `message` diz qual e o `hint` diz o que
serve.

Acontece com: `sort` fora da lista do recurso; `order` diferente de `asc`/`desc`; `status` de factura
fora dos estados válidos; datas (`date_from`, `date_to`, `updated_since`) que não são ISO 8601;
`customer_id`/`invoice_id` não numéricos.

**Não** acontece com `per_page` acima de 100 — esse é reduzido a 100 em silêncio.

**O que fazer:** corrija o parâmetro. O `hint` traz a lista de valores válidos; não a adivinhe.

### validation_failed

**HTTP 422.** O corpo de uma escrita não passou na validação. É o único erro com um campo extra,
`error.errors`: um mapa `campo → lista de mensagens`.

```json
{
  "error": {
    "code": "validation_failed",
    "message": "Os dados enviados não passaram na validação.",
    "hint": "Corrija os campos indicados em \"errors\" e repita o pedido — pode usar a MESMA chave de idempotência, porque um pedido recusado não cria nada.",
    "doc_url": "https://developers.fintra.co.mz/erros#validation_failed",
    "request_id": "…",
    "errors": {
      "items.0.quantity": ["The items.0.quantity field must be at least 0.0001."],
      "items.0.cost_price": ["Campo não aceite numa linha de factura. Campos aceites: product_id, name, description, quantity, price, discount, tax_id."]
    }
  }
}
```

As chaves de `errors` seguem a notação com pontos das linhas: `items.0.price` é o `price` da
**primeira** linha da factura (contagem a partir de 0).

⚠️ **As mensagens dentro de `errors` vêm em inglês** quando são as validações genéricas do
servidor (`The … field must be …`); só as regras próprias desta API é que trazem texto em português.
Não as mostre ao utilizador final tal como vêm, e não escreva código que dependa do texto delas —
o que é estável é a **chave** (`items.0.quantity`), não a frase.

**O que fazer:** corrija os campos e repita. **Pode reutilizar a mesma `Idempotency-Key`** — um
pedido recusado não criou nada, logo a chave não ficou gasta.

---

## 400 e 409 — a idempotência

### idempotency_key_required

**HTTP 400.** Um `POST` ou `PUT` sem o cabeçalho `Idempotency-Key`. É obrigatório em **todas** as
escritas, sem excepção.

**O que fazer:** gere um UUID v4 e envie-o em `Idempotency-Key`. Guarde-o do seu lado antes de fazer
o pedido: se a rede falhar, é ele que lhe permite repetir sem duplicar.

### idempotency_key_invalid

**HTTP 400.** O cabeçalho veio, mas com um formato que não serve.

Regras: 8 a 191 caracteres; apenas letras, dígitos, `.`, `_`, `:` e `-`. Sem espaços, sem `/`, sem
acentos. Um UUID v4 cumpre tudo isto.

**O que fazer:** use `uuidgen`, `Str::uuid()`, `uuid.uuid4()` ou equivalente. Não construa a chave
concatenando o nome do cliente.

### idempotency_request_in_flight

**HTTP 409.** Já existe um pedido a decorrer com esta mesma chave, e ainda não terminou.
Vem com `Retry-After: 2`.

Acontece quando o seu código repete demasiado cedo, ou quando dois processos seus disparam o mesmo
pedido ao mesmo tempo.

**O que fazer:** espere os segundos indicados e **repita com a MESMA chave** — receberá a resposta
original. **Não gere uma chave nova**: isso criaria um segundo registo, que é exactamente o que a
idempotência existe para evitar. Este é o único 409 em que repetir é a acção certa.

### idempotency_key_reuse

**HTTP 409.** Esta chave já criou um registo, mas com um **corpo diferente** do que enviou agora.

Uma chave de idempotência serve **um** pedido. Repetir o mesmo pedido, sim; enviar outro pedido com a
chave antiga, não.

⚠️ **"Corpo diferente" é medido byte a byte**, sobre o texto cru do pedido — não sobre o JSON
interpretado. `{"phone":"+258849990001"}` e `{"phone": "+258849990001"}` (um espaço) são, para este
efeito, dois pedidos diferentes, e o segundo dá `409`. Se a sua rotina de repetição volta a serializar
o objecto, pode gerar bytes diferentes sem querer — sobretudo se a ordem dos campos não for estável.
**Guarde a string exacta que enviou e reenvie essa.**

**O que fazer:** gere um UUID novo para este pedido. E confirme o seu código: reutilizar a chave
entre pedidos diferentes é normalmente um bug de uma variável que não foi reiniciada dentro do ciclo.
As chaves são guardadas 24 horas; passado esse prazo ficam livres.

---

## 429

### rate_limit_exceeded

**HTTP 429.** Excedeu um dos limites. Vem com **`Retry-After`** (segundos) e, quando aplicável,
`X-RateLimit-Limit` e `X-RateLimit-Remaining`.

| Limite | Valor |
| --- | --- |
| Pedidos por minuto, por chave | 60 |
| Escritas por minuto, por chave | 10 |
| Pedidos por dia, por chave | 5 000 |
| Pedidos por minuto, por empresa | 120 |
| Pedidos por minuto sem chave válida, por IP | 30 |

**O que fazer:** leia o `Retry-After`, espere esse tempo, repita. Depois disso, faça *backoff*
exponencial (2×, 4×, 8× o intervalo) e desista ao fim de 5 tentativas com um erro no seu log.

Não repita em ciclo apertado: cada tentativa recusada continua a contar para o limite e só adia o
momento em que voltaria a passar. Se bate no tecto diário, o problema é o desenho — use
`updated_since` para sincronizar só o que mudou, em vez de reler tudo.
