# API de Parceiros Fintra — v1

Esta API deixa um sistema externo ler e criar dados de **uma empresa** que usa a Fintra:
clientes, produtos, facturas e pagamentos.

É **servidor-para-servidor**. Não é uma API de browser, não é uma API de utilizador final,
e não substitui a Fintra: há coisas que continuam a acontecer dentro da aplicação, por uma pessoa.

## O que esta API faz, e o que não faz

| Faz | Não faz |
| --- | --- |
| Ler clientes, produtos, facturas e pagamentos | Emitir facturas fiscais |
| Criar e alterar clientes | Registar pagamentos |
| Criar e alterar produtos | Apagar seja o que for |
| Criar facturas **em rascunho** | Alterar ou cancelar facturas |

Três limites permanentes da v1, e é melhor sabê-los antes de desenhar a integração:

1. **As facturas nascem em rascunho** (`status: "draft"`, `invoice_code: "DRAFT-XXXXXXXX"`).
   A emissão do documento fiscal é feita por uma pessoa, dentro da Fintra. Não existe nenhum
   endpoint de emissão — não o procure e não tente contorná-lo com outro verbo.
2. **Pagamentos são só de leitura.** `GET /payments` e `GET /payments/{id}` existem;
   `POST /payments` não existe e devolve 404.
3. **Nada se apaga.** Não há `DELETE` em nenhum recurso.

## Base

```
https://api.fintra.co.mz/api/v1/partner
```

Todas as respostas são JSON, em UTF-8. Datas com hora vêm em ISO 8601
(`2026-08-03T10:14:22+00:00`); datas sem hora vêm como `2026-08-03`.

## Autenticação

Uma chave, num cabeçalho:

```
Authorization: Bearer fintra_sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

O que precisa de saber sobre a chave:

- **A chave é da EMPRESA, não da pessoa.** Ela não representa um utilizador da Fintra, não tem
  perfil e não muda de empresa. Tudo o que ela alcança é o que pertence à empresa a que foi emitida.
- **É emitida pelo suporte Fintra** (`suporte@fintra.co.mz`) e mostrada **uma única vez**. Se a
  perder, pede-se uma nova; não há como recuperá-la.
- **Prefixos:** `fintra_sk_live_` (dados reais) e `fintra_sk_test_` — leia a secção *Modo de teste*
  antes de contar com esta última.
- **Scopes:** cada chave leva uma lista de permissões, uma por recurso e verbo:
  `customers:read`, `customers:write`, `products:read`, `products:write`, `invoices:read`,
  `invoices:write`, `payments:read`. Uma chave só de leitura não escreve, e um pedido fora do
  scope devolve `403 insufficient_scope`. Peça ao suporte exactamente os scopes de que precisa.
- **A API de parceiros vem desligada em cada empresa.** Se receber `403 partner_api_disabled`,
  o suporte ainda não a activou nessa empresa.

## Regra número um: a chave nunca vai para um browser

**Não use esta API a partir de JavaScript numa página.** Nem numa app móvel que embrulhe uma página,
nem "só para o painel interno", nem "só em desenvolvimento".

Uma chave `fintra_sk_live_...` dentro de código que corre num browser é uma chave publicada: qualquer
visitante abre o inspector e fica com os livros da empresa — clientes, facturas, valores.

A API impede-o, e sem lhe perguntar a opinião: **qualquer pedido que traga o cabeçalho `Origin`
leva `403 browser_use_forbidden`** antes de tocar em dados. Um `fetch()` de browser envia sempre
`Origin`; um cliente de servidor nunca envia.

Chame a API a partir do seu servidor, guarde a chave numa variável de ambiente, e nunca a
escreva num ficheiro versionado.

## Modo de teste — a verdade

As chaves `fintra_sk_test_...` existem, mas **hoje só lêem**.

Não existe empresa-sandbox: uma chave de teste aponta para os **dados reais** da mesma empresa, e a
única forma honesta de a manter inofensiva é bloquear-lhe a escrita. Qualquer `POST` ou `PUT` com
uma chave de teste devolve `403 test_mode_read_only`.

Consequência prática: **para desenvolver a parte de escrita da sua integração precisa de uma chave
`live`**, e o que ela criar é real (rascunhos reais, clientes reais). Faça-o numa empresa de ensaio
da sua conta, não na do cliente. Um sandbox com dados próprios está previsto, mas não existe — não
desenhe a sua integração a contar com ele.

## Idempotência — obrigatória em todas as escritas

Todo o `POST` e todo o `PUT` exigem o cabeçalho `Idempotency-Key`. Sem ele: `400 idempotency_key_required`.

```
Idempotency-Key: 3f2a9c1e-7b44-4f0e-9a1d-6c5e2b8d0417
```

Porquê: se a rede falhar depois de a Fintra ter gravado, mas antes de a resposta lhe chegar, você
não sabe se o registo existe. Com a chave, basta repetir o pedido **igual, com a mesma chave**:
recebe a resposta original — o mesmo corpo e o mesmo estado HTTP (um `POST` repetido volta a
responder `201`, não `200`) — mais o cabeçalho `Idempotency-Replay: true`, e não se cria um segundo
registo.

Regras:

- Gere um **UUID v4 novo por cada pedido novo**.
- 8 a 191 caracteres, apenas letras, dígitos, `.`, `_`, `:` e `-`.
- A mesma chave com um corpo **diferente** → `409 idempotency_key_reuse`.
- ⚠️ **"Igual" quer dizer byte a byte.** A comparação é feita sobre o corpo cru do pedido, não sobre
  o JSON interpretado: um espaço a mais, uma quebra de linha ou os campos por outra ordem já contam
  como um pedido diferente e dão `409`. Na sua rotina de repetição **reenvie exactamente a mesma
  string** que enviou da primeira vez — guarde-a, em vez de voltar a serializar o objecto.
- A mesma chave enquanto o primeiro pedido ainda corre → `409 idempotency_request_in_flight`; espere e repita.
- A chave é guardada **24 horas**. Passado esse prazo fica livre outra vez — não a reutilize à espera do replay.

Gerar um UUID v4:

```bash
uuidgen                                        # macOS, Linux
```
```php
$key = \Illuminate\Support\Str::uuid()->toString();   // PHP / Laravel
```
```python
import uuid; key = str(uuid.uuid4())                  # Python
```

## Paginação

Todas as listas são paginadas. Não existe forma de pedir "tudo".

| Parâmetro | Default | Máximo |
| --- | --- | --- |
| `page` | 1 | — |
| `per_page` | 25 | **100** |

Pedir `per_page=1000` **não é erro**: é reduzido a 100 em silêncio. Confirme sempre o `per_page` que
vem na resposta em vez de assumir o que pediu.

```json
"pagination": { "total": 214, "count": 25, "per_page": 25, "current_page": 1, "last_page": 9 }
```

Percorra até `current_page == last_page`. Para sincronizações incrementais use `updated_since`
(clientes, produtos, facturas) em vez de reler tudo.

## Ordenação

`sort` e `order` (`asc` | `desc`). Cada recurso aceita uma lista fechada de campos —
um campo fora da lista devolve `422 invalid_query_parameter` com a lista válida dentro do `hint`.

## Erros

Todas as falhas — todas, sem excepção — têm a mesma forma:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "A chave não tem permissão para esta operação.",
    "hint": "Esta chave não inclui o scope \"invoices:write\". Peça ao suporte Fintra uma chave que o inclua.",
    "doc_url": "https://developers.fintra.co.mz/erros#insufficient_scope",
    "request_id": "0f1c2f26-9b7e-4a6b-8f2a-1d3c5b7e9a01"
  }
}
```

Escreva **um** parser de erros: `error.code` é estável e é por ele que deve ramificar o seu código.
`message` e `hint` são para humanos e podem mudar. O `request_id` vem também no cabeçalho
`X-Request-Id` — guarde-o nos seus logs, é por ele que o suporte Fintra encontra o pedido.

A lista completa está no **[catálogo de erros](./erros)**.

## Ids de outra empresa dão 404

Um id que não existe, um id mal formado e um id **de outra empresa** devolvem exactamente o mesmo
`404 resource_not_found`. É deliberado: se a resposta distinguisse os casos, a API dizia
"existe, mas não é seu" — e isso já é informação sobre os dados de outra empresa.

Portanto: um 404 **não** prova que o registo não existe. Prova que a sua chave não lhe chega.

## Limites de tráfego

Por chave:

| Limite | Valor |
| --- | --- |
| Pedidos por minuto | 60 |
| Escritas por minuto | 10 |
| Pedidos por dia | 5 000 |

Por empresa (somando todas as chaves dela): 120 pedidos/minuto.
Pedidos sem chave válida, por IP: 30/minuto.

Ao exceder recebe `429 rate_limit_exceeded` com o cabeçalho **`Retry-After`** em segundos.
**Respeite-o**: espere esse tempo exacto e repita. Não repita em ciclo apertado — cada tentativa
recusada conta na mesma para o limite e afasta-o do momento em que voltaria a passar.

Um cliente decente faz *exponential backoff* a partir do `Retry-After` e nunca mais de 5 tentativas.

## Primeiro pedido

`GET /ping` confirma, de uma vez, que a chave é válida, qual a empresa a que ela pertence, em que
modo está e que scopes tem:

```bash
curl -s https://api.fintra.co.mz/api/v1/partner/ping \
  -H "Authorization: Bearer $FINTRA_API_KEY"
```

```json
{
  "data": {
    "tenant_id": 36,
    "tenant_name": "Bar Kanimambo",
    "mode": "live",
    "scopes": ["customers:read","customers:write","products:read","products:write","invoices:read","invoices:write","payments:read"],
    "customers_visible": 12,
    "request_id": "6a2f0f4e-0e40-4a0a-9c6a-2c1b8d5f7a10"
  }
}
```

Se isto responder, a integração pode começar. Continue nos **[endpoints](./endpoints)**.

---

*A escrever a integração com a ajuda de uma IA? Dê-lhe primeiro o ficheiro
[llms.txt](./llms.txt) — traz estas regras em forma de instruções.*
