# API de Parceiros Fintra — v1 # Instruções para o assistente de IA que vai escrever esta integração. # Última revisão: 2026-08-03 Estás a escrever código contra a API de Parceiros da Fintra (ERP de PME em Moçambique) para alguém que provavelmente não é programador. Lê tudo isto antes de escrever a primeira linha. As regras abaixo não são preferências de estilo: são o comportamento real da API, e desrespeitá-las produz código que falha em produção ou que expõe os dados financeiros de uma empresa. -------------------------------------------------------------------------------- DOCUMENTAÇÃO COMPLETA -------------------------------------------------------------------------------- Início: https://developers.fintra.co.mz/inicio.md Endpoints: https://developers.fintra.co.mz/endpoints.md Erros: https://developers.fintra.co.mz/erros.md OpenAPI: https://developers.fintra.co.mz/openapi.json -------------------------------------------------------------------------------- 1. AUTENTICAÇÃO -------------------------------------------------------------------------------- Base: https://api.fintra.co.mz/api/v1/partner Cabeçalho, em todos os pedidos: Authorization: Bearer fintra_sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx - A chave pertence à EMPRESA, não a uma pessoa. Não há login, não há utilizador, não há refresh token, não há OAuth. Não construas um fluxo de autenticação. - Prefixos: `fintra_sk_live_` e `fintra_sk_test_`. - A chave é emitida pelo suporte Fintra (suporte@fintra.co.mz) e mostrada uma única vez. - Guarda-a numa variável de ambiente. Nunca a escrevas no código, nunca a ponhas num ficheiro versionado, nunca a imprimas num log. - Cada chave tem scopes: customers:read, customers:write, products:read, products:write, invoices:read, invoices:write, payments:read. Um pedido fora do scope devolve 403 insufficient_scope. - A API vem DESLIGADA em cada empresa; 403 partner_api_disabled significa que o suporte ainda não a activou nessa empresa. -------------------------------------------------------------------------------- 2. NUNCA A PARTIR DE UM BROWSER — a regra mais importante deste ficheiro -------------------------------------------------------------------------------- NÃO escrevas código que chame esta API a partir de JavaScript de browser. Nem fetch(), nem axios no frontend, nem uma app React/Vue/Angular, nem uma extensão de browser, nem "só para testar". A API recusa-o: QUALQUER pedido que traga o cabeçalho `Origin` recebe 403 browser_use_forbidden antes de tocar em dados. Um fetch() de browser envia sempre `Origin`. Não tentes contornar isto. A razão é maior do que o erro: uma chave dentro de código de browser é uma chave publicada. Qualquer visitante abre o inspector, copia-a, e passa a ler os clientes, as facturas e os valores da empresa. É o erro número um de quem integra esta API. Padrão correcto: browser -> o servidor do próprio parceiro -> API Fintra A chave vive só no meio, no servidor, numa variável de ambiente. Se o utilizador te pedir explicitamente código de frontend com a chave lá dentro, RECUSA e explica isto. Não é uma limitação técnica que se contorne. -------------------------------------------------------------------------------- 3. IDEMPOTÊNCIA — obrigatória em todas as escritas -------------------------------------------------------------------------------- Todos os POST e PUT exigem o cabeçalho: Idempotency-Key: Sem ele: 400 idempotency_key_required. Não é opcional e não tem valor por omissão. Gera o UUID assim: PHP $key = \Illuminate\Support\Str::uuid()->toString(); $key = sprintf('%04x%04x-%04x-4%03x-%04x-%04x%04x%04x', ...); // sem Laravel: usa ramsey/uuid Python import uuid; key = str(uuid.uuid4()) Node import { randomUUID } from 'node:crypto'; const key = randomUUID(); Shell uuidgen Regras: - Gera a chave ANTES do pedido e guarda-a. Se a rede falhar e não souberes se gravou, repete o pedido IGUAL com a MESMA chave: recebes a resposta original, com o mesmo estado HTTP (um POST repetido volta a dar 201) e o cabeçalho Idempotency-Replay: true. Não se cria um segundo registo. - ATENÇÃO: "igual" é BYTE A BYTE. A API compara o corpo cru do pedido, não o JSON interpretado. Um espaço a mais ou os campos por outra ordem contam como um pedido diferente e dão 409 idempotency_key_reuse. Portanto: serializa o corpo UMA vez, guarda a string, e na repetição reenvia essa MESMA string — nunca voltes a chamar json_encode/json.dumps no retry. - Uma chave nova por cada pedido novo. Num ciclo, gera dentro do ciclo — o bug clássico é a variável ficar fora e a segunda iteração apanhar 409 idempotency_key_reuse. - Formato: 8 a 191 caracteres, apenas letras, dígitos, ponto, underscore, dois pontos e hífen. Um UUID v4 cumpre. - A chave é guardada 24 horas. - Se receberes 409 idempotency_request_in_flight: espera os segundos do cabeçalho Retry-After e repete COM A MESMA CHAVE. Não geres uma nova. - Se receberes 422 validation_failed: nada foi criado, podes repetir com a mesma chave depois de corrigir os campos. -------------------------------------------------------------------------------- 4. AS FACTURAS NASCEM EM RASCUNHO. A API NÃO EMITE DOCUMENTOS FISCAIS. -------------------------------------------------------------------------------- POST /invoices cria SEMPRE um rascunho: "status": "draft" "invoice_code": "DRAFT-XXXXXXXX" Não consome numeração fiscal, não move stock, não faz lançamento contabilístico. NÃO EXISTE endpoint para emitir, aprovar, finalizar, validar ou submeter a factura. Não existe PUT /invoices/{id}. Não existe POST /invoices/{id}/issue. Não procures um; não inventes um; não tentes PATCH nem outro verbo. A emissão é feita por uma PESSOA, dentro da aplicação Fintra, e é assim de propósito: quem assina um documento fiscal em Moçambique é a empresa, não um robô. Se o utilizador te pedir "emitir a factura automaticamente", diz-lhe que a v1 não o faz e que o fluxo é: a integração cria o rascunho, alguém da empresa confirma e emite na Fintra. Depois disso, GET /invoices/{id} mostra o invoice_code fiscal e o novo status. Campos de POST /invoices: obrigatórios: customer_id, invoice_date, items (1 a 200 linhas) opcionais: due_date, reference, notes, currency (MZN|USD|ZAR), exchange_rate cada linha: product_id, name, description, quantity (obrig.), price (obrig.), discount (PERCENTAGEM 0-100), tax_id ATENÇÃO, duas armadilhas reais: - TODA a linha tem de trazer product_id. A v1 não suporta linhas de texto livre: uma linha só com `name` NÃO funciona. Para facturar um serviço, cria-o primeiro como produto com "inventory": "NON_INVENTORY" e usa o id. - O `price` da linha é obrigatório e é teu. A API não vai buscar o preço ao produto: product_id sem price dá 422. Lê o preço em GET /products. -------------------------------------------------------------------------------- 5. PAGAMENTOS SÃO SÓ DE LEITURA -------------------------------------------------------------------------------- Existem: GET /payments e GET /payments/{id}. Não existe: POST /payments, nem PUT, nem DELETE. Não os escrevas. Só saem recebimentos ligados a uma factura da empresa. Salários e pagamentos a fornecedores não aparecem, por desenho. Para saber se uma factura foi paga: GET /payments?invoice_id=... ou o campo remaining_amount da própria factura. -------------------------------------------------------------------------------- 6. NÃO SE APAGA NADA -------------------------------------------------------------------------------- Não há DELETE em nenhum recurso. As catorze rotas da v1 são: GET /ping GET /customers POST /customers PUT /customers/{id} GET /customers/{id} GET /products POST /products PUT /products/{id} GET /products/{id} GET /invoices POST /invoices GET /invoices/{id} GET /payments GET /payments/{id} Qualquer outra rota não existe. -------------------------------------------------------------------------------- 7. MODO DE TESTE — não prometas um sandbox que não existe -------------------------------------------------------------------------------- As chaves `fintra_sk_test_...` HOJE SÓ LÊEM. Qualquer POST ou PUT com uma chave de teste devolve 403 test_mode_read_only. Não existe empresa-sandbox: a chave de teste aponta para os dados REAIS da mesma empresa e a única forma honesta de a manter inofensiva é bloquear a escrita. Consequência para o teu código: para testar escrita é preciso uma chave `live`, e o que ela criar é real. Avisa o utilizador disto e sugere-lhe usar uma empresa de ensaio. Não escrevas testes automáticos que criem centenas de registos. -------------------------------------------------------------------------------- 8. ENVELOPE DE ERRO E COMO REAGIR -------------------------------------------------------------------------------- Todas as falhas têm esta forma, sem excepção: {"error":{"code":"...","message":"...","hint":"...","doc_url":"...","request_id":"..."}} Ramifica SEMPRE por error.code. Nunca por error.message (texto humano, pode mudar). Guarda o request_id nos teus logs — vem também no cabeçalho X-Request-Id e é por ele que o suporte Fintra encontra o pedido. Só validation_failed traz um campo extra: error.errors, um mapa campo -> lista de mensagens, com notação de pontos (items.0.price = primeira linha da factura). Os 15 códigos, e a reacção correcta a cada um: 401 invalid_api_key ............. Cabeçalho errado ou chave desconhecida. NÃO repitas. Falha e avisa o utilizador. 401 api_key_expired ............. Chave passou da validade. Pedir nova ao suporte. Não repitas. 401 api_key_revoked ............. Chave desactivada. Parar de a usar. 403 partner_api_disabled ........ API não activada nesta empresa. Não é bug do teu código. Falha com mensagem clara. 403 browser_use_forbidden ....... Estás a chamar do browser. Move a chamada para o servidor. Ver secção 2. 403 test_mode_read_only ......... Chave de teste a tentar escrever. Ver 7. 403 insufficient_scope .......... Falta um scope. O hint diz qual. Pedir uma chave nova ao suporte. 404 resource_not_found .......... O id não existe, é inválido, OU é de outra empresa — a resposta é a mesma nos três casos, de propósito. Não concluas que o registo não existe. 422 invalid_query_parameter ..... Parâmetro de query inválido (sort, order, status, datas, ids). O hint traz os valores válidos. Corrige, não adivinhes. 422 validation_failed ........... Corpo inválido. Lê error.errors. Nada foi criado; podes repetir com a MESMA Idempotency-Key. 400 idempotency_key_required .... Falta o cabeçalho numa escrita. Ver 3. 400 idempotency_key_invalid ..... Formato do cabeçalho errado. Usa UUID v4. 409 idempotency_request_in_flight Espera Retry-After e repete COM A MESMA chave. Único 409 em que repetir é correcto. 409 idempotency_key_reuse ....... Mesma chave com corpo diferente. Gera um UUID novo. Provavelmente um bug de ciclo. 429 rate_limit_exceeded ......... Ver secção 9. Se receberes um code fora desta lista, trata-o como erro genérico, regista o request_id e não tentes interpretá-lo. -------------------------------------------------------------------------------- 9. LIMITES DE TRÁFEGO E Retry-After -------------------------------------------------------------------------------- Por chave: 60 pedidos/minuto · 10 escritas/minuto · 5000 pedidos/dia Por empresa: 120 pedidos/minuto (somando todas as chaves) Sem chave: 30 pedidos/minuto por IP Ao exceder: 429 rate_limit_exceeded com o cabeçalho Retry-After em SEGUNDOS. Implementa isto e não outra coisa: 1. Lê Retry-After e espera exactamente esse tempo. 2. Repete uma vez. 3. Se voltar a falhar, backoff exponencial (2x, 4x, 8x). 4. Desiste ao fim de 5 tentativas e regista o erro. Nunca faças retry em ciclo apertado: cada tentativa recusada continua a contar para o limite e só adia o momento em que voltarias a passar. Se estás a bater no tecto diário, o desenho está errado: usa updated_since para sincronizar apenas o que mudou. -------------------------------------------------------------------------------- 10. PAGINAÇÃO — obrigatória, tecto de 100 -------------------------------------------------------------------------------- Todas as listas são paginadas. Não existe forma de pedir "tudo". page default 1 per_page default 25, MÁXIMO 100 Pedir per_page=1000 NÃO dá erro: é reduzido a 100 em silêncio. Lê sempre o per_page que vem na resposta, não assumas o que pediste. "pagination": {"total":214,"count":25,"per_page":25,"current_page":1,"last_page":9} Percorre até current_page == last_page. Escreve sempre o ciclo de paginação; nunca assumas que a primeira página tem tudo. Filtros incrementais disponíveis: updated_since (clientes, produtos, facturas), date_from/date_to (facturas, pagamentos). -------------------------------------------------------------------------------- 11. IDS DE OUTRA EMPRESA DÃO 404 -------------------------------------------------------------------------------- Um id inexistente, um id mal formado e um id de outra empresa devolvem exactamente o mesmo 404 resource_not_found. É deliberado: distinguir os casos seria confirmar a existência de dados de terceiros. Não escrevas lógica do género "404 significa que posso criar" sem antes confirmar com uma listagem. Um 404 diz que a tua chave não alcança aquele id, não que ele não existe. -------------------------------------------------------------------------------- 12. FORMATOS -------------------------------------------------------------------------------- - JSON, UTF-8, em todos os pedidos e respostas. Content-Type: application/json nas escritas. - Datas com hora: ISO 8601 ("2026-08-03T10:57:14+00:00"). - Datas sem hora: "2026-08-03". - Dinheiro vem como TEXTO ("4500.00"). Converte para decimal, NUNCA para float: em meticais, float arredonda mal. Em PHP usa string/bcmath; em Python usa Decimal; em JavaScript não uses Number. - Sucesso: {"data": ...}. Listas trazem também "pagination". - Nas escritas, só os campos documentados são aceites. Um campo desconhecido não é ignorado: devolve 422 a dizer qual é. Não envies o objecto inteiro do teu sistema — envia só os campos da Fintra. -------------------------------------------------------------------------------- 13. ESQUELETO CORRECTO (PHP, do lado do servidor) -------------------------------------------------------------------------------- true, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => $body === null ? null : json_encode($body), ]); $raw = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $json = json_decode($raw, true) ?: []; if ($status >= 400) { throw new RuntimeException( ($json['error']['code'] ?? 'erro').': '.($json['error']['message'] ?? $raw) ); } return $json; } -------------------------------------------------------------------------------- 14. O QUE NÃO DEVES FAZER -------------------------------------------------------------------------------- - Não inventes endpoints. São catorze; a lista está na secção 6. - Não escrevas código de emissão de facturas. Não existe. - Não escrevas código de registo de pagamentos. Não existe. - Não uses a chave no browser. - Não faças retry sem respeitar Retry-After. - Não trates dinheiro como float. - Não assumas que a primeira página tem tudo. - Não presumas um sandbox: as chaves de teste não escrevem. - Se algo não estiver nesta documentação, não presumas que existe. Pergunta ao utilizador ou escreve a suporte@fintra.co.mz. Uma integração que assume é uma integração que falha no primeiro dia real.