# Operação e contrato detalhado Este documento descreve a primeira versão implementada. Ele complementa [`integration.md`](./integration.md) e o contrato [`openapi.json`](./openapi.json). ## Respostas e retentativas Não deduza o pagamento pelo HTTP: uma repetição idempotente pode retornar HTTP 200 com uma transação `failed`, `unknown` ou `pending`. Verifique sempre `data.status`, `data.simulated` e `data.environment`. | HTTP / código | O que aconteceu | Ação correta | | --- | --- | --- | | `201` | Nova cobrança registrada e resultado inicial recebido | Salve ID e consulte estado | | `200` + `Idempotency-Replayed: true` | A mesma tentativa já existe | Use a transação retornada; não houve nova submissão | | `202` / `processing` | Tentativa reservada e processamento ainda não concluído | Aguarde e consulte; não crie outra chave | | `202` / `unknown` | Provedor pode ter criado a cobrança, mas resposta é incerta | Reconcilie; não envie uma nova cobrança | | `400 invalid_json` | JSON inválido/nulo, campos duplicados/desconhecidos, valor incompatível ou mais de um objeto | Corrija o payload | | `400 idempotency_key_required` | Header ausente ou fora de `^[A-Za-z0-9._:-]{1,128}$` | Persista e envie uma chave válida | | `401 unauthorized` | Chave/sessão ausente, revogada, expirada ou inválida | Corrija autenticação; não tente em loop | | `401 invalid_credentials` | Login incorreto | Corrija e-mail/senha | | `403 insufficient_scope` | A chave não permite a operação | Emita chave com os scopes necessários | | `403 forbidden` | Papel do usuário não permite a operação | Use um perfil autorizado | | `403 tenant_suspended` | Tenant ou um ancestral suspenso | Trate com o administrador | | `403 live_payments_disabled` | Trava de pagamentos reais ativa | Administrador deve decidir sobre o ambiente; não contorne a trava | | `403 live_payment_out_of_scope` | Cobrança fora da autorização restrita configurada no servidor | Use somente tenant, provider, referência, chave de idempotência e valor autorizados | | `403 origin_denied` | Origem do navegador não permitida | Ajuste a configuração CORS no servidor | | `404 not_found` | ID inexistente ou fora do escopo | Confira ID e tenant; não revele dados de outra árvore | | `409 idempotency_conflict` | Mesma chave, payload diferente | Recupere payload original. Nova chave somente para nova tentativa comercial deliberada | | `409 reconciliation_required` | Não há ID externo para consultar | Use a referência interna `txn_...` para localizar a cobrança no provedor | | `409 provider_status_conflict` | Atualização externa regrediria estado final | Reconcilie manualmente; estado local é preservado | | `409 simulation_not_allowed` | Transação não é pendente do simulador | Não tente simular pagamento real ou reabrir estado final | | `413 request_too_large` | Corpo JSON ultrapassou 1 MiB | Reduza o corpo antes de reenviar | | `422 validation_error` | Campo inválido | Corrija os dados; veja OpenAPI e a mensagem | | `422 tenant_from_api_key` | Criação pública incluiu outro tenant | Remova `tenant_id`; use chave emitida para o tenant alvo | | `422 provider_not_configured` | Nenhuma regra/provider habilitado para o método | Confira regra e enabled_methods do provider | | `422 provider_method_disabled` | Regra tenta usar/ativar um método desabilitado no provider | Administrador revisa enabled_methods antes de criar ou habilitar a regra | | `422 subaccount_not_configured` | Provider marketplace sem vínculo ativo e habilitado do tenant exato | Super admin configura a subconta desse tenant; não usar conta do pai | | `422 subaccount_credentials_missing` | Vínculo sem credencial utilizável do ambiente | Recuperar a credencial por procedimento seguro; refresh cadastral não garante recuperar token perdido, e a chave da matriz não é fallback | | `422 provider_pix_sandbox_unsupported` | Pix Safe2Pay selecionado em sandbox | Use simulador local ou configure produção explicitamente; boleto possui sandbox remoto | | `422 simulator_disabled` | Simulador desativado/incompatível | Confira `ENABLE_SIMULATOR` e ambiente | | `422 amount_limit_exceeded` | Valor acima do máximo da regra | Administrador revisa a regra; não divida cobrança automaticamente | | `429 rate_limit_exceeded` | Limite por credencial/IP atingido | Aguarde no mínimo 60 s, com jitter; preserve chave/payload | | `502 provider_rejected` | Rejeição conhecida na criação; registro local `failed` preservado | Consulte GET pelo `error.transaction_id` e os diagnósticos; a mesma chave/corpo recupera o registro sem reenvio. HTTP 200 no replay não significa sucesso financeiro | | `502 provider_refresh_failed` | Consulta ao provedor falhou | Estado local preservado; repita a consulta com espera progressiva | | `503 payment_settings_unavailable` | Não foi possível ler/gravar o controle de pagamentos em produção | Emissão e refresh remoto em produção ficam bloqueados; restaurar acesso ao banco, sem fallback para autorização do ambiente | | `503 rate_limiter_unavailable` | Redis indisponível | Acesso falha de forma fechada. Recupere Redis e repita com a mesma idempotência | | `503 reconciliation_required` | Solicitação foi reservada, mas persistência do resultado falhou | Preserve a chave e investigue a transação; não reprocesse pagamento | | `503 not_ready` | PostgreSQL/Redis não respondem no readiness | Retire a instância do balanceamento e recupere a dependência | | `500 internal_error` ou falha de rede | Resposta não confirmada | Preserve a chave, consulte e repita somente a tentativa original | Conflitos adicionais de cadastro, como `type_has_children`, `cannot_suspend_self` e violações de unicidade/referência, devem ser corrigidos pelo operador. Endpoints não existentes e métodos HTTP incorretos podem receber uma resposta padrão do roteador, que não é o envelope JSON de erro da aplicação. O serviço não garante `Retry-After`. O SDK o expõe quando presente, mas o integrador deve ter uma espera mínima própria para 429. Um backoff recomendado para consultas é 5, 10, 20 e 30 segundos com jitter, respeitando o rate limit. Não faça retries automáticos que troquem a chave idempotente. ## Validação e normalização - Corpos JSON aceitam somente os campos documentados. JSON nulo, objetos adicionais e campos duplicados (inclusive variantes de maiúsculas/minúsculas) são recusados com 400. O limite de 1 MiB é aplicado ao corpo completo; excesso retorna 413. - `amount` deve ser um inteiro JSON. Frações ou strings são rejeitadas. - `reference`, `description` e nome do pagador têm espaços externos removidos. - Documento recebe trim e conversão para maiúsculas. Aceita 11 ou 14 caracteres alfanuméricos sem pontuação. Esta validação de formato não confere dígitos verificadores nem comprova identidade. - E-mail recebe trim e conversão para minúsculas. A validação básica exige `@` e até 254 bytes; o provedor pode impor critérios adicionais. - Novas cobranças Pix exigem telefone não vazio, somente dígitos após trim e no máximo 20 bytes no valor original, sem mínimo de 10/11 dígitos. Boleto exige telefone não vazio de até 20 bytes após trim. Nome tem 2 a 120 bytes; referência, 1 a 100; descrição, 1 a 200. O limite do Go conta bytes UTF-8: acentos podem ocupar mais de um byte. - Novos Pix e boletos exigem endereço: CEP de 8 dígitos, logradouro de 2 a 120 bytes, número de 1 a 20, bairro/cidade de 2 a 80 e UF brasileira válida. Complemento é opcional, até 120 bytes. Em Pix, trim e UF em maiúsculas são aplicados em uma cópia para validar e preparar o corpo remoto; o telefone/endereço originais continuam no hash e no snapshot da API. - Omissão ou zero em `expires_in` usa 3.600; valores explícitos de 60 a 86.400 definem a duração. - A comparação idempotente usa esse objeto estruturado normalizado, incluindo tenant e expiração. A ordem das propriedades JSON não afeta o hash. - A chave idempotente é armazenada no banco junto da transação e não possui limpeza automática por TTL. Replays Pix de registros anteriores continuam aceitos com o corpo e a chave originais, mesmo sem telefone/endereço; não reenviam ao provider. Alterar o corpo na mesma chave retorna `409 idempotency_conflict`. Os novos requisitos são aplicados a novas emissões antes da reserva/chamada externa. O SDK `createPix` é estrito; uma repetição legada deliberada usa `request` de baixo nível com o corpo original salvo, sem preencher dados nem gerar nova chave. O GET local permanece disponível. ## Isolamento e papéis | Capacidade | `super_admin` | `tenant_admin` | `viewer` | Chave pública | | --- | --- | --- | --- | --- | | Ler transações | Global | Próprio tenant e filhos | Próprio tenant e filhos | Scope `transactions:read`; listagem dos filhos por opt-in | | Criar Pix | Tenant escolhido | Próprio tenant ou filho permitido | Não | `pix:write`, somente tenant da chave | | Criar boleto | Tenant escolhido | Próprio tenant ou filho permitido | Não | `boleto:write` explícito, somente tenant da chave | | Atualizar estado pelo provedor | Global | Própria árvore | Não | transactions:read + scope de escrita do método; própria árvore | | Configurar tenants/tipos/usuários | Sim | Não | Não | Não | | Ver/configurar providers e regras | Sim | Não | Não | Não | | Criar/importar/consultar/alterar subcontas Safe2Pay | Sim | Não | Não | Não | | Ver/alterar controle global de pagamentos em produção | Sim | Não | Não | Não | | Emitir/revogar chaves | Global | Própria árvore | Não | Não | | Ler logs/notificações | Global | Própria árvore | Própria árvore | Não | | Criar notificação interna | Sim | Não | Não | Não | | Marcar notificação lida | Sim | No próprio escopo | No próprio escopo | Não | As relações de tenant são uma árvore. O tipo do pai precisa permitir filhos. `parent_id` e `type_id` são imutáveis após criação nesta versão; não há endpoint de mover tenant. Suspender um ancestral impede a autenticação/uso de credenciais dos descendentes. Consultas a recursos de outra árvore usam 404 para não expor sua existência. No GET público de uma transação específica, o pai pode consultar um filho sem o parâmetro `include_descendants`. Esse parâmetro controla a **listagem**. Com `tenant_id` na listagem, o tenant indicado passa a ser a raiz do filtro; `include_descendants=true` inclui a subárvore dele. ## Roteamento de pagamento A configuração de providers e regras é exclusiva do **super admin**, inclusive pela API administrativa. Administradores e usuários dos tenants não podem alterá-la. A API pública define o tenant pela chave e não aceita `provider_id` na emissão: o backend escolhe o provider pelas regras abaixo. Para configurar um tenant pelo painel: 1. Em **Providers**, cadastre ou localize o provider, habilite-o e habilite os métodos desejados: Pix e/ou boleto. 2. Em **Tenants**, localize o tenant e abra **Configurar providers**. 3. Clique em **Adicionar regra**. O tenant já estará pré-selecionado; escolha o método e o provider, informe a prioridade e o limite por cobrança e salve a regra habilitada. 4. Se o tenant usar os dois métodos, configure uma regra para cada um. Uma regra na software house pode atender seus clientes por herança, sem cadastrar a mesma regra em cada filho. A seleção para cada nova cobrança segue esta ordem: 1. Procura regras ativas para o método solicitado e providers habilitados no tenant com a capacidade presente em `enabled_methods`. 2. Se não houver, sobe pelos ancestrais, dando preferência ao ancestral mais próximo. 3. Se não houver regra de um ancestral, usa regra global (`tenant_id: null`). 4. Dentro do mesmo nível, escolhe menor `priority`, depois a data de criação mais antiga e o ID. Uma regra do filho sempre precede a regra da software house, mesmo que tenha um número de prioridade maior. 5. Verifica modo permitido e `max_amount` da regra selecionada. Para trocar o provider de um tenant, crie uma nova regra para o mesmo tenant e método e desative a anterior. O provider, o tenant e o método de uma regra existente não são editáveis; a prioridade, o limite e a ativação são. Evite deixar regras concorrentes com a mesma prioridade, pois a mais antiga continuará vencendo. **Desativar uma regra, um provider ou um método não bloqueia necessariamente as cobranças do tenant.** O candidato desativado sai da seleção, e outra regra do próprio tenant, da software house ou global pode assumir. Verifique as regras herdadas antes de usar a desativação como medida operacional. Alterações de roteamento valem para novas cobranças. Transações já emitidas continuam vinculadas ao provider original; repetir a mesma solicitação com a mesma chave de idempotência recupera essa transação, e a atualização de status consulta o provider registrado nela. ### Providers marketplace e subcontas Providers Safe2Pay podem usar `is_marketplace: true`. Após selecionar a regra, a emissão exige vínculo ativo e habilitado do tenant exato com esse provider, incluindo a credencial do ambiente configurado. Uma regra herdada não concede a subconta do ancestral. A transação persiste `subaccount_id`; o refresh usa esse vínculo original. A ausência de vínculo não autoriza usar a chave da matriz como fallback. A gestão de subcontas usa a chave de **produção da matriz marketplace**, mesmo em provider `environment=sandbox`. Pagamentos nesse ambiente continuam usando `TokenSandbox` da subconta. A recusa Safe2Pay `301` na criação indica uso da chave sandbox da matriz; corrigir a credencial de gestão não requer ativar pagamentos reais nem mudar o ambiente. Somente super admin cria, importa, consulta e altera subcontas. `POST /admin/subaccounts` reserva o registro antes da chamada externa, com idempotência por provider + chave; estados `creating`/`unknown` exigem consulta ou reconciliação, não outro Add automático. `/refresh` consulta por ID remoto; `/link` confere ID/documento/tokens retornados pelo marketplace e pode recuperar um registro incerto sem duplicar, somente quando as credenciais forem verificáveis. Tokens são cifrados com AAD do ID local da subconta e nunca aparecem no DTO, logs ou respostas brutas. O cadastro sanitizado ainda contém dados pessoais/bancários e deve ter acesso restrito. Refresh e atualização cadastral preservam segredos locais utilizáveis; valores remotos mascarados não podem substituí-los nem comprovar a credencial de produção no link. As leituras recalculam `has_production_token` e `has_sandbox_token`; máscara ou cifra ilegível resulta em `false`. Preserve backups e a chave de criptografia. A Safe2Pay informa que o token é retornado na criação e, se perdido, exige atendimento do suporte; não prometa recuperá-lo por consulta. Consulte [preservação e perda de tokens](subaccounts.md#preservação-e-perda-de-tokens) para limites e erros de importação. Para salvar uma credencial recuperada ou reemitida pela Safe2Pay, o super admin usa **Atualizar token** na subconta, ou `PATCH /admin/subaccounts/{id}/credentials` com `api_key` e `expected_updated_at` do GET local. O ID é local; apenas o token do ambiente do provider é substituído, preservando demais segredos e a identidade do vínculo. A escrita é cifrada e auditada sem token. Não há chamada remota, prova de titularidade/validade ou habilitação automática, nem emissão de cobrança. Concorrência retorna `409 subaccount_changed`; não repita automaticamente com um timestamp novo. A recuperação de `unknown` limita-se ao motivo `subaccount_credentials_missing`. Veja o [procedimento administrativo](subaccounts.md#atualizar-o-token-local-pelo-super-admin). Não há sandbox cadastral isolado prometido. Atualização com `MerchantSplit` substitui todas as taxas/meios; omissão preserva. Desabilitar um vínculo é local e não exclui a subconta remota. Não é permitido trocar a credencial da matriz ou retirar `is_marketplace` enquanto houver vínculos não `failed` (`409 marketplace_has_subaccounts`). Uma rejeição definitiva `failed` sem ID remoto permite nova tentativa deliberada corrigida com nova chave, preservando o histórico; isso nunca se aplica a `creating`/`unknown`. Consulte [subaccounts.md](subaccounts.md) para procedimentos, campos e erros. O DTO guarda `last_provider_error_code` (string de 1 a 10 dígitos ou nulo) e `last_error_message` (mensagem estática sanitizada ou nula), além do código interno `last_error_code`. Criação, consulta, listagem e link também apresentam motivo seguro e código em `error.message`, quando disponíveis. Não armazenam nem expõem os textos brutos `Error`/`Message` da Safe2Pay. Códigos desconhecidos mantêm orientação genérica e devem ser correlacionados por request ID, código e IDs locais. A migração `004_subaccount_errors.sql` adiciona os campos; o diagnóstico não altera decisões de idempotência ou estados incertos. O limite excedido não seleciona silenciosamente outro provedor. Não há failover de submissão para evitar cobrar em um segundo provedor após uma resposta incerta. A regra global do simulador é criada no bootstrap quando habilitado. Confira o provider selecionado no registro da transação. Providers usam `enabled_methods: ["pix"]` por padrão; boleto precisa de opt-in e regra `method: "boleto"`. `[]` desabilita os métodos, valores duplicados/desconhecidos são inválidos. Chaves existentes conservam os scopes anteriores; emissão de boleto exige `boleto:write` explícito. Idempotência usa o mesmo namespace tenant/chave para ambos os métodos; mudar método com a mesma chave gera conflito. Veja [campos e limites de boleto](./boleto.md). ### Controle de pagamentos em produção no super admin Em **Providers → Pagamentos em produção**, o super admin consulta o controle global, altera `allow_live_payments` e salva. A alteração é persistida e auditada no banco; a API lê o valor dinamicamente, sem cache ou necessidade de reinício. A auditoria `settings.payments.updated` registra valores anterior/novo, administrador e request ID na mesma transação da alteração. Usuários de tenants, viewers e chaves públicas não podem ler nem alterar essa configuração. Os endpoints administrativos são `GET /admin/settings/payments` e `PATCH /admin/settings/payments`, com sessão Bearer de `super_admin`. O PATCH exige apenas `{ "allow_live_payments": false }` ou `{ "allow_live_payments": true }`. Omissão, `null` ou campos extras não são aceitos. O exemplo abaixo consulta e desabilita; não cria cobrança: ```sh : "${CALEBE_API_URL:?Defina a origem da API}" : "${CALEBE_ADMIN_TOKEN:?Defina uma sessão super_admin}" curl --silent --show-error --fail-with-body \ "$CALEBE_API_URL/admin/settings/payments" \ -H "Authorization: Bearer $CALEBE_ADMIN_TOKEN" curl --silent --show-error --fail-with-body -X PATCH \ "$CALEBE_API_URL/admin/settings/payments" \ -H "Authorization: Bearer $CALEBE_ADMIN_TOKEN" \ -H 'Content-Type: application/json' \ --data '{"allow_live_payments":false}' ``` A resposta de ambos usa `{data: ...}` com os seguintes campos: | Campo | Significado | | --- | --- | | `allow_live_payments` | Valor efetivo do controle global. | | `source` | `database` quando há valor salvo; `environment` somente quando o registro não existe. | | `environment_default` | Padrão `ALLOW_LIVE_PAYMENTS` carregado do ambiente. Não substitui um valor salvo. | | `allow_unscoped_live_payments` | Indicador da flag de autorização sem escopo configurada no servidor; somente leitura. | | `has_live_payment_scope` | Indica presença de escopo válido no servidor; o conteúdo do escopo não é devolvido. | | `issuance_authorized` | `allow_live_payments` habilitado junto com escopo ou autorização explícita sem escopo. Ainda depende de tenant, rota, provider, método, valor e credenciais válidos. | | `updated_at`, `updated_by` | Data RFC3339 e ID do administrador que salvou; nulos quando não disponíveis, como no fallback/autor do bootstrap. | **Salvar `allow_live_payments: true` sozinho não libera pagamentos reais.** A cobrança ainda precisa obedecer a `LIVE_PAYMENT_SCOPE_JSON`, quando configurado, ou ao opt-in explícito `ALLOW_UNSCOPED_LIVE_PAYMENTS=true`. Essas duas configurações continuam no servidor, sem edição pelo portal. O painel mostra os indicadores para explicar por que uma emissão continua bloqueada. A migração `005` adiciona `platform_settings`. O bootstrap inicializa o controle a partir de `ALLOW_LIVE_PAYMENTS` apenas se ainda não houver registro; reiniciar, fazer deploy ou mudar essa variável não sobrescreve o valor salvo pelo super admin. Para alterar uma instalação já inicializada, use o portal ou o PATCH. A leitura usa o padrão de ambiente apenas quando o registro está ausente. Se o banco falhar, a API responde `503 payment_settings_unavailable` e bloqueia a emissão em produção, em vez de assumir permissão. O controle é verificado na **criação e no refresh remoto de transações em produção**. Se estiver desabilitado, essas operações retornam `403 live_payments_disabled`; uma falha ao consultar o banco retorna `503 payment_settings_unavailable`. O GET local de transações continua disponível. Simulador e boleto sandbox não consultam esse controle global e mantêm suas regras próprias. Esse controle não altera ambientes de providers, regras, métodos, escopos de chaves, credenciais ou subcontas. A mudança passa a valer nas próximas verificações; não cancela uma emissão já enviada ao provider nem cobranças já criadas. Também não substitui a reconciliação de uma operação incerta. ### Teste real limitado a uma cobrança Para uma emissão pontual autorizada, configure `LIVE_PAYMENT_SCOPE_JSON` no servidor e habilite o controle efetivo `allow_live_payments` no super admin. O escopo restringe a emissão à combinação exata de tenant, provider, referência, chave de idempotência, valor em centavos e método: ```dotenv LIVE_PAYMENT_SCOPE_JSON='{"tenant_id":"ten_autorizado","provider_id":"prv_autorizado","reference":"TESTE-PIX-1","idempotency_key":"teste-pix-1-v1","amount":100}' ``` Essa combinação pode produzir somente uma cobrança enquanto seu registro idempotente permanecer no banco. Uma repetição recupera o registro existente; outro valor ou outra chave é bloqueado. A regra do tenant continua sendo verificada e pode impor seu próprio limite. O JSON inválido ou incompleto impede a inicialização do servidor. O escopo também impede consultas financeiras a outro provider. O simulador mantém seu comportamento próprio. `method` é opcional para compatibilidade e assume `"pix"`; para autorizar boleto real, informe explicitamente `"method":"boleto"`. Um escopo Pix não autoriza boleto. Safe2Pay boleto sandbox usa a credencial sandbox e não exige liberar produção. O controle efetivo `allow_live_payments=false` bloqueia operações reais mesmo quando há um escopo configurado. Se o controle estiver ativo, mas não houver escopo nem `ALLOW_UNSCOPED_LIVE_PAYMENTS=true`, `issuance_authorized` permanece falso e a emissão é recusada. A configuração de ambiente `ALLOW_LIVE_PAYMENTS=true` sem escopo também continua sujeita à validação de inicialização do servidor. A flag sem escopo é `false` por padrão e deve permanecer assim neste teste restrito. Não remova o escopo para resolver erros ou gerar outra tentativa. ## Reconciliação e atualizações O registro `processing` é gravado e confirmado no PostgreSQL **antes** da chamada externa. A restrição única `(tenant_id, idempotency_key)` protege chamadas concorrentes e múltiplas instâncias. A chamada ao provedor continua por um prazo limitado mesmo se o cliente desconectar. Uma queda do processo depois da reserva pode deixar `processing` pendente. Não existe worker de recuperação automática nesta versão. O operador deve localizar o pedido pela referência externa: a Calebe Pay envia seu próprio ID local `txn_...` no campo Safe2Pay `Reference`; o `reference` comercial do cliente é mantido localmente. Se `provider_reference` existe, `/refresh` consulta o provedor. Sem esse ID, a API retorna `reconciliation_required`; a consulta manual na Safe2Pay evita emitir outra cobrança. ### Diagnóstico da criação e da consulta Transações incluem `provider_error_code`, `provider_error_message` e `provider_http_status`, todos nullable. O código remoto aceita somente string de 1 a 10 dígitos; a mensagem é uma orientação estática segura da Calebe Pay, de até 400 caracteres; HTTP remoto aceita 100 a 599. Não são armazenados corpos brutos `Error`/`Message`, tokens, QR ou dados pessoais dentro desses campos. A migração `006_payment_diagnostics.sql` deixa registros anteriores com valores nulos. Uma recusa confirmada na criação retorna `502 provider_rejected` e `error.transaction_id`, mesmo quando a resposta remota usa HTTP 200 ou `HasError: false` com status de cobrança recusada. Resultado incerto permanece `202` com `data.status: "unknown"`; os diagnósticos não transformam timeout em certeza de falha. Replay de `failed`/`unknown` retorna `200` com o registro existente; apenas `processing` continua `202` no replay. Nunca há reenvio ao provider nesse replay. Os erros de pagamento podem incluir `transaction_id` e os três diagnósticos diretamente no objeto `error`, além de `code`, `message` e `request_id`. Validações anteriores à reserva não possuem ID local de transação. `request_id` continua exclusivo da requisição/auditoria, não do DTO de transação. Se uma proteção local impedir o envio depois da reserva, a mensagem identifica bloqueio local e o HTTP remoto permanece nulo. Uma falha da consulta ao provider no refresh retorna os diagnósticos **daquela consulta** em `error`, com o ID local, mas preserva o estado e o diagnóstico de criação já salvos. Não confunda a recusa de uma consulta atual com o motivo que originou uma cobrança anterior. GET local nunca faz chamada externa e é o primeiro passo para ler o registro existente. Se os detalhes da recusa antiga não foram capturados, o diagnóstico permanece nulo. Não há como reconstruí-lo a partir do valor, do horário ou do status genérico `failed`. Um novo POST financeiro cria outra tentativa e não recupera o erro histórico; não use uma nova chave apenas para investigar. ### JSON de criação e corpo preparado para envio `GET /v1/transactions/{id}/request`, ou `/admin/transactions/{id}/request` no painel, consulta os snapshots locais `request_payload` e `provider_request_payload`. O primeiro guarda o corpo normalizado com tenant resolvido; o segundo guarda o corpo preparado pelo builder do provider. Ambos são persistidos na reserva, antes de uma possível chamada externa. A resposta usa `source: "captured"` e `missing_fields: []`; `provider_request` é `null` para o simulador. Isso não prova que a transmissão ocorreu, que o provider aceitou o pedido ou que houve pagamento. Registros anteriores retornam `source: "legacy_partial"`: somente campos recuperáveis, como valor, referência e pagador conhecido, sem reconstruir o payload remoto ou supor opções antigas. `missing_fields` descreve as lacunas, incluindo expiração Pix ou opções de boleto não armazenadas. Consulte o [contrato e os campos exatos](integration.md#consultar-o-json-da-requisição). Não crie uma nova cobrança para preencher esse histórico nem use o objeto parcial como payload de retry. A autorização é a mesma da leitura de transação: scope `transactions:read` para chave e isolamento pela hierarquia, com `404` fora do escopo. Tokens, headers e chave de idempotência não fazem parte dos snapshots expostos; dados pessoais do pagador fazem. Os corpos ficam no endpoint de detalhe, não nas listagens gerais. Restrinja acesso, cópias, retenção e exportação desses dados; não registre o JSON completo em logs ou chamados de suporte. ### Consulta Safe2Pay por referência sem reenvio A Safe2Pay documenta `GET https://api.safe2pay.com.br/v2/transaction/reference?reference=` com `X-API-KEY`. O retorno usa `HasError` e `ResponseDetail: {TotalItems, Objects: [...]}`. Essa operação pode localizar uma cobrança existente sem fazer outro POST de emissão. [Consulta oficial por referência](https://developers.safe2pay.com.br/reference/cobranca-buscar-por-refer%C3%AAncia). Para cobranças originadas na Calebe Pay, a referência remota é o ID **local `txn_...`**, não o `reference` comercial do pedido. O operador deve consultar na mesma conta emissora; em marketplace, use a credencial da subconta original e o ambiente correto. As credenciais continuam no backend/cofre e nunca devem ser fornecidas ao pagador ou ao frontend. A consulta retorna uma lista: confira referência, valor e identificador de cada resultado, sem assumir que o primeiro item pertence à tentativa. Resultado vazio não recupera o motivo de uma recusa anterior e não autoriza uma nova emissão automática. A documentação não promete guardar ou pesquisar o erro de uma validação que impediu a criação. Este endpoint remoto não é um novo endpoint público Calebe Pay; o `/refresh` implementado continua exigindo o `provider_reference` conhecido. Na [documentação oficial de erros v2](https://developers.safe2pay.com.br/reference/erros-de-api-v2), o exemplo usa `ErrorCode` e `Error`, enquanto a tabela descreve `Message`. O adaptador trata essas variantes sem expor o texto bruto. O catálogo consultado não fornece um mapeamento completo e específico de recusas Pix; códigos desconhecidos ficam disponíveis para rastreabilidade com orientação genérica, sem inferir KYC, saldo ou habilitação comercial. O [contrato de criação](https://developers.safe2pay.com.br/reference/cobranca-criar) fornece exemplos de `ResponseDetail.Status`, `Message` e `Description`; um HTTP isolado não substitui o estado financeiro. O refresh preserva estados finais: `paid` só avança para `refunded`; `expired`, `failed` e `refunded` não reabrem automaticamente. Respostas que regrediriam o estado são recusadas. A atualização usa o estado anterior na condição SQL para evitar que consultas concorrentes sobrescrevam uma confirmação mais recente. Na consulta Safe2Pay, ID externo, valor exato e referência interna `txn_...` precisam coincidir com o registro local. Uma divergência retorna `provider_refresh_failed` sem alterar o estado. Um status externo não reconhecido não é tratado como confirmação de cobrança pendente ou paga. No simulador, `/refresh` expira uma transação pendente cujo prazo passou. `POST /admin/transactions/{id}/simulate` aceita `paid` ou `expired` apenas para uma transação pendente simulada, com o simulador habilitado. Essa ação não chama a Safe2Pay. Em boleto, `due_date` é uma data de calendário. No simulador, `cancel_after_due=true` calcula expiração no início do dia posterior ao vencimento mais `days_before_cancel`, em São Paulo; sem essa opção `expires_at` é nulo. Em provider remoto, a consulta confirma o status bancário. O status 2 da Safe2Pay para boleto é `processing`, sem comprovar liquidação. Artefatos de boleto recebidos são validados: URL HTTPS, linha de 47–48 dígitos, código de 44 dígitos e vencimento esperado. ## Observabilidade e infraestrutura `GET /admin/summary?days=30` devolve indicadores e série diária da árvore autorizada. `days` aceita inteiros de 1 a 365 e usa 30 por padrão. Totais monetários, contagens de transações, taxa de sucesso e série usam a mesma janela móvel de criação (`created_at >= now() - days`); o campo `days` da resposta confirma a janela. A série agrupa dias em `America/Sao_Paulo`. Contagens de tenants refletem o cadastro atual e não a janela. `tenant_id` seleciona uma subárvore permitida. - `GET /health`: processo respondendo. - `GET /ready`: verifica PostgreSQL e Redis com timeout. - `GET /metrics`: métricas Prometheus `calebe_http_requests_total` e `calebe_http_errors_total`. - `X-Request-ID`: correlação por requisição, também incluída nos erros e logs estruturados. - Auditoria no banco: criação/alteração de configurações, chaves e operações de transação. A reserva da cobrança e seu primeiro evento são escritos na mesma transação SQL. No desenvolvimento local, o processo Go usa `HOST=127.0.0.1` e a origem pública `http://localhost:18080`. No contêiner, o bind pode usar todas as interfaces internas, com a publicação da porta controlada pelo Compose. O bind local não publica a API na rede externa. O Redis faz rate limiting por ator: 120 requisições por minuto. Login: 10 tentativas por minuto por IP direto da conexão. Não existe confiança automática em `X-Forwarded-For`; atrás de proxy, reveja essa política antes de escalar. Métricas e endpoints de saúde não exigem login; restrinja a exposição do endpoint de métricas na borda da rede. ## Segurança implementada e limites de produção Senhas são verificadas com bcrypt. Chaves de API e sessões são guardadas como hash. Segredos de provedores são cifrados com AES-GCM e vinculados ao ID do provider. A chave de cifragem do ambiente precisa de armazenamento e backup seguros: perdê-la impede usar os segredos salvos. Trocar apenas a variável não recifra os registros antigos. Sessões duram oito horas, podem ser revogadas no logout e são invalidadas ao trocar a senha ou suspender o usuário. Suspender um tenant revoga permanentemente as sessões de toda sua subárvore; suas API keys ficam bloqueadas enquanto o tenant/ancestral estiver suspenso e voltam a funcionar após reativação. A alteração e a revogação de sessões são atômicas; reativar não restaura sessões anteriores. Não há MFA, recuperação de senha, convite de usuário ou SSO nesta versão. A configuração de CORS não substitui autenticação. Sirva a API por HTTPS fora do desenvolvimento, defina origens explicitamente e remova credenciais de bootstrap de ambientes compartilhados após provisionar. O banco armazena dados pessoais do pagador; esta versão não implementa mascaramento de dados em repouso, política automática de retenção/eliminação, autorização por campos, RLS PostgreSQL nem processo completo de atendimento a direitos do titular. O isolamento de tenants é aplicado pelos handlers e consultas SQL. Antes de produção, avalie acesso operacional ao banco, backups, rotação de segredos, testes de carga, revisão independente de segurança e operação do parceiro de pagamentos. Esta versão implementa emissão e consulta de Pix/boleto e gestão administrativa de subcontas Safe2Pay. Não implementa livro razão financeiro, saldo custodial, liquidação, split dinâmico por transação, conciliação bancária automática, entrega de webhooks, cobrança recorrente, cartões ou endpoints de cancelamento/estorno. Taxas estáticas da subconta não equivalem a esses serviços. O estado de uma cobrança não representa saldo disponível. O simulador não certifica uma integração financeira real.