# Calebe Pay — instruções para integração por agentes Objetivo: integrar um sistema cliente à API pública implementada nesta versão. Leia integration.md, boleto.md e openapi.json antes de gerar código. Para gestão administrativa de subcontas, leia também subaccounts.md. operations.md contém as limitações operacionais. Todos os caminhos abaixo são relativos à origem da API, inicialmente http://localhost:18080. ## Regras que não podem ser inferidas de outro gateway - Moeda BRL. amount é número inteiro de centavos; nunca string e nunca reais decimais. - Corpos JSON: no máximo 1 MiB (excesso: 413 request_too_large); null, campos desconhecidos/duplicados e múltiplos objetos são 400 invalid_json. - API pública: X-API-Key. A chave é da Calebe Pay, não da Safe2Pay. - POST /v1/pix e POST /v1/boleto criam no tenant da chave. Não inclua tenant_id ou provider_id nesses corpos. - Idempotency-Key é obrigatório, padrão ^[A-Za-z0-9._:-]{1,128}$. Salve uma chave por tentativa comercial de pagamento ANTES da rede. Reutilize chave e payload em falhas de transporte; não gere UUID novo em um retry. - reference é correlação do pedido, não é a chave de idempotência. - Novas cobranças exigem customer.name, document, email, phone e address. Documento: 11 ou 14 caracteres alfanuméricos sem pontuação. Preserve documento e telefone como strings; as regras específicas de Pix e boleto estão abaixo. - Novo Pix exige customer.phone não vazio, somente dígitos após trim, até 20 bytes no valor original e sem mínimo de 10/11 dígitos. Exige customer.address: zip_code 8 dígitos, street 2–120 bytes, number 1–20, district/city 2–80, state UF brasileira; complement opcional até 120. Validação de endereço usa cópia com trim/UF uppercase; hash/snapshot Calebe preservam phone/address originais e o builder remoto normaliza. - Pix: expires_in em segundos, de 60 a 86400, padrão 3600. Backend monta PaymentMethod='6', IsSandbox=false, Application, Products e PaymentObject.Expiration automaticamente. Não exigir esses campos internos no corpo público. Boleto não aceita expires_in; exige customer.phone não vazio (até 20 bytes após trim), customer.address e boleto.due_date em YYYY-MM-DD, de hoje até 365 dias à frente em São Paulo na primeira emissão. Mantenha o vencimento original nos retries. - Replay Pix antigo sem telefone/endereço continua válido com corpo e chave originais, sem reenvio ao provider; corpo alterado na mesma chave é 409. SDK createPix exige PaymentCustomer completo e valida antes do fetch. Para replay legado explícito use client.request('/v1/pix',{method:'POST',body:corpoOriginal,idempotencyKey:chaveOriginal}); não preencher campos nem fazer fallback automático. Nova chave com corpo incompleto é 422. Não usar legacy_partial como corpo original. - Endereço de boleto: zip_code com 8 dígitos, street, number, district, city e state com UF brasileira; complement é opcional. O objeto boleto aceita instruction de até 200 bytes, penalty_rate e interest_rate de 0 a 100 com até duas casas decimais, cancel_after_due e days_before_cancel de 0 a 120. Veja os limites detalhados em openapi.json e boleto.md. - Providers usam enabled_methods: ["pix"] por padrão; [] é permitido. Boleto precisa ser explicitamente habilitado e ter regra method=boleto. Chaves recebem pix:write e transactions:read por padrão; boleto:write exige opt-in. - Sucesso objeto: {data: objeto}. Lista: {data: array, meta:{limit,offset,count}}. Erro: {error:{code,message,request_id}}. - Erro de pagamento após reserva também pode trazer transaction_id e provider_error_code/provider_error_message/provider_http_status diretamente dentro de error, sem objeto metadata. Antes da reserva, esses campos podem estar ausentes. request_id não existe no DTO de transação. - Transaction inclui provider_error_code (string numérica de 1 a 10 dígitos ou null), provider_error_message (texto estático seguro até 400 caracteres ou null) e provider_http_status (HTTP remoto 100 a 599 ou null). HTTP remoto 200 pode corresponder a recusa; nunca confirma pagamento. - Nova recusa confirmada retorna 502 provider_rejected com transaction_id, inclusive HasError=false com status remoto de falha. Incerteza continua 202 com data.status=unknown, sem reenvio automático. SDK retorna Transaction em 202. - GET local pelo ID lê diagnóstico persistido sem chamada externa. Replay da mesma chave/corpo retorna registro failed com HTTP200 e não reenvia ao provider. Detalhes antigos não capturados ficam null; não inferir causa nem gerar outra cobrança para recuperar erro histórico. - Falha da consulta ao provider em refresh descreve a consulta atual em error com transaction_id e diagnósticos; não sobrescreve o status nem o diagnóstico de criação salvos. Replay unknown também200; só processing replay202. - Operador pode consultar o endpoint remoto oficial GET /v2/transaction/reference?reference= com credencial da conta emissora original; não é endpoint público Calebe Pay. Resposta ResponseDetail.TotalItems/Objects pode localizar transação existente, mas não promete recuperar recusa pré-criação. Resultado vazio não autoriza novo POST. Veja operations.md. - listTransactions do SDK mantém envelope; createPix/createBoleto/getTransaction/refreshTransaction retornam data diretamente. Filtro method=pix|boleto está disponível na listagem. - GET /v1/transactions/{id}/request (alias /admin com Bearer) retorna data {transaction_id,source,request,provider_request,missing_fields}. Exige transactions:read e a mesma hierarquia do GET transação; fora do escopo 404. SDK getTransactionRequest(id, signal?) retorna TransactionRequest diretamente. Somente leitura local, sem chamada externa. - source=captured: request normalizado, tenant_id resolvido e defaults; provider_request preparado com os mesmos builders da emissão, ambos persistidos na reserva. Não são bytes originais nem prova de envio/aceite/pagamento. Simulator retorna provider_request null; captured missing_fields=[]. Não contém headers, tokens ou Idempotency-Key, mas contém dados pessoais; não expor em listagens gerais/logs. - source=legacy_partial: somente dados conhecidos, provider_request null. missing_fields no Pix: expires_in/provider_request; no boleto: boleto.instruction, boleto.penalty_rate, boleto.interest_rate, boleto.cancel_after_due, boleto.days_before_cancel, provider_request e boleto.due_date se desconhecido. Não inventar defaults ou reemitir para recuperar histórico. Captured Pix inclui expires_in efetivo (default 3600); boleto não inclui expires_in e pode omitir defaults zero/false. - CalebePayError do SDK mantém status/code/message/requestId/retryAfter e expõe transactionId/providerErrorCode/providerErrorMessage/providerHttpStatus, nullable. Não analisar texto livre para decidir retry; preservar a chave original. - Boleto retorna boleto_url, boleto_digitable_line, boleto_barcode e boleto_due_date (strings ou null). Preserve linha e código de barras como strings. Os três artefatos são nulos no simulador local. - GET /v1/transactions/{id} lê o estado local. POST /v1/transactions/{id}/refresh exige transactions:read e o scope do método: pix:write ou boleto:write. processing em boleto pode representar intenção de pagamento sem liquidação; só paid confirma. - O namespace idempotente tenant + key é compartilhado por Pix e boleto. Trocar método com a mesma chave gera 409. Não gere outra chave para contornar esse erro; persista uma tentativa deliberada por pedido e método. - Não existe webhook de saída, endpoint de cartão, split dinâmico por transação, saldo, saque, liquidação ou solicitação de cancelamento/estorno nesta versão. Taxas estáticas de uma subconta não equivalem a esses serviços. Não invente esses endpoints. - Não use mensagens do frontend para confirmar pagamento. Leia o status pelo seu backend. - A API pública lista o próprio tenant por padrão. include_descendants=true permite consultar descendentes dentro do escopo; isso não permite escrever no tenant filho. Escrita pública no filho exige chave do filho. ## Ambientes Safe2Pay informa que Pix NÃO possui sandbox: https://developers.safe2pay.com.br/reference/cobranca-criar simulator + sandbox: Pix e boleto locais, simulated=true, sem artefatos pagáveis. Nunca chame isso de sandbox remoto Safe2Pay. safe2pay + production: exige credencial, allow_live_payments efetivo habilitado e LIVE_PAYMENT_SCOPE_JSON com tenant_id, provider_id, reference, idempotency_key, amount e method exatos. method omitido significa pix; boleto requer "method": "boleto". Produção geral sem escopo exige também ALLOW_UNSCOPED_LIVE_PAYMENTS=true; não remova o escopo nem ative esse flag automaticamente. Pode gerar cobrança real. Divergência retorna 403 live_payment_out_of_scope. safe2pay + sandbox: aceita boleto com credencial sandbox (simulated=false, environment=sandbox, sem confirmação financeira real); Pix é recusado com provider_pix_sandbox_unsupported. ## Fluxo mínimo correto 1. Obter CALEBE_API_URL e CALEBE_API_KEY do ambiente do servidor. Falhar claramente se a chave não existir. 2. Criar pedido e persistir payload/chave idempotente. 3. POST /v1/pix ou /v1/boleto com Content-Type: application/json, X-API-Key e Idempotency-Key. 4. Guardar data.id, method, status, simulated, environment e artefatos do método. 5. Exibir cobrança pendente ou indicação explícita de simulação. 6. Consultar /refresh de forma limitada pelo servidor; usar GET para leitura local. 7. Liberar produto somente ao confirmar paid em production pelo backend. Tratar unknown como pendência de reconciliação; simulated ou sandbox nunca confirma dinheiro real. 8. Para timeout ou 5xx, preservar chave/payload. Não assumir que não houve cobrança. ## Segurança Nunca solicitar segredos reais em chat ou commitar .env. Nunca colocar API key em VITE_*, EXPO_PUBLIC_*, localStorage, app mobile ou Electron renderer. Esses clientes chamam o backend do integrador. O painel Calebe Pay usa Bearer em /admin e /auth; login retorna token temporário. Não transformar tokens de painel em chaves públicas. Senha alterada ou usuário suspenso revoga as sessões anteriores. Nos clientes, ignore respostas de sessões antigas após logout/novo login; um 401 atrasado não pode remover a sessão nova. Não enviar credencial a URLs fornecidas por pagadores. O SDK trava a origem configurada e rejeita redirecionamentos. Ao logar falhas, usar code/request_id/status e redigir dados pessoais. ## Controle global de produção: somente super admin - GET/PATCH /admin/settings/payments exigem sessão Bearer de super_admin; nenhuma chave pública de tenant pode usar essas rotas. - PATCH aceita só {"allow_live_payments":boolean}, obrigatório. Não enviar null, escopo detalhado ou outras flags. - Resposta data: allow_live_payments, source (database|environment), environment_default, allow_unscoped_live_payments, has_live_payment_scope, issuance_authorized, updated_at e updated_by nullable. - O valor salvo no banco tem efeito sem reinício/cache e é auditado. Bootstrap usa ALLOW_LIVE_PAYMENTS do ambiente apenas se o registro não existe; deploy/restart não sobrescreve a escolha salva. - Criação e refresh remoto em produção consultam esse controle: false retorna 403 live_payments_disabled; falha no banco retorna 503 payment_settings_unavailable. GET local continua disponível. Fallback de ambiente só para registro ausente, nunca para erro de consulta. - allow_live_payments=true sozinho não autoriza emissão: LIVE_PAYMENT_SCOPE_JSON ou ALLOW_UNSCOPED_LIVE_PAYMENTS=true continuam exigidos no servidor. O escopo detalhado não é exposto nem editável nessa API. issuance_authorized não comprova rota/credenciais válidas. - Não habilitar esse controle automaticamente para corrigir erros. Não editar valores do banco diretamente. A alteração vale nas próximas verificações e não cancela uma emissão já enviada. Simulador e boleto sandbox não consultam esse controle global. ## Subcontas Safe2Pay: administração separada - Somente sessão Bearer de super_admin. Nenhuma chave de tenant, tenant_admin ou viewer pode gerir subcontas. Não existe /v1/subaccounts nem scope público administrativo. - Provider kind=safe2pay, is_marketplace=true. Cada tenant que transaciona precisa de vínculo próprio com esse provider; nunca herda subconta de pai. A regra de roteamento pode ser herdada; a credencial financeira não. - api_key do provider marketplace é a chave de PRODUÇÃO da MATRIZ para gestão, mesmo com environment=sandbox. Pagamentos usam Token ou TokenSandbox da subconta conforme environment. Não usar a chave sandbox da matriz no cadastro nem substituir a chave da matriz pelo token de um cliente. Não converter providers existentes automaticamente. - POST /admin/subaccounts recebe tenant_id, provider_id, registration com nomes originais Safe2Pay. Exige Idempotency-Key persistida; namespace provider + chave separado de pagamentos. Corpo diferente na mesma chave é 409. Mesma chave não gera novo Add. - registration tem Name, Identity, Email, ResponsibleBirthDate, Address e campos condicionais. PJ exige ResponsibleName e ResponsibleIdentity; CPF assume os próprios. Não enviar Token, SecretKey, Integration, senha ou campos desconhecidos. - GET /admin/subaccounts e /admin/subaccounts/{id} são locais; filtros tenant_id/provider_id/status são exatos. POST /{id}/refresh consulta remoto pelo external_id; nunca cria. Preserva segredos locais utilizáveis; somente valores remotos completos e não mascarados podem preencher lacunas. Sem ID, reconciliation_required exige importação verificada. - GET /admin/providers/{id}/subaccounts/remote lista somente id remoto, name, document e email; offset precisa ser múltiplo de limit. POST /admin/subaccounts/link exige ID/documento/tokens verificáveis com o marketplace. Máscara enviada retorna 422 validation_error; token original sem confirmação remota completa retorna 422 subaccount_credentials_unverifiable, inclusive se GET só devolver máscara. api_key e sandbox_api_key são write-only; nunca registrar esses valores. - PATCH /admin/subaccounts/{id} com enabled altera apenas o vínculo local; não exclui a conta remota. Habilitação exige active e token do ambiente. - PUT /admin/subaccounts/{id}/registration exige Email e ResponsiblePhone. MerchantSplit omitido preserva taxas; presente substitui a configuração completa. Aceita MerchantPaymentDate; não pode alterar Name/Identity ou responsável legal. Omitir opções booleanas preserva a configuração atual. Preserva credenciais utilizáveis existentes, inclusive na confirmação por GET; não é rotação de token. - TaxTypeName "1" é percentual (0 a 100); "2" é valor em reais, não centavos. Não inventar taxas para habilitar meios. Criar sem MerchantSplit não habilita Pix/boleto automaticamente. - creating/unknown não autorizam nova criação com outra chave. Ler estado, fazer refresh se houver ID ou link verificado; preservar corpo/chave. 202 é incerteza, não sucesso cadastral definitivo. - subaccount_busy exige aguardar operação em curso; subaccount_changed indica concorrência e exige nova leitura. Uma consulta atrasada não deve sobrescrever alterações mais recentes. - Cadastro remoto não é simulador nem sandbox isolado prometido. TokenSandbox é credencial para transações suportadas. Não executar cadastro real como teste automático. - Tokens ficam cifrados e nunca são devolvidos. DTO recalcula has_production_token/has_sandbox_token a partir das credenciais locais: máscara ou cifra ilegível resulta em false; true não comprova validade remota nem homologação. registration ainda contém dados pessoais/bancários. - A Safe2Pay documenta Token apenas na criação e reposição via suporte em caso de perda. GET pode devolver máscara; refresh/PUT não garantem recuperar token perdido. Preservar backups e chave de criptografia. Depois de obter o segredo correto, o super admin pode atualizar somente sua cópia local; isso não gera token na Safe2Pay. - PATCH /admin/subaccounts/{id}/credentials recebe api_key completo e expected_updated_at RFC3339 copiado exatamente do GET local; ambos obrigatórios. id é local sub_..., não ID remoto. Atualiza apenas Token para provider production ou TokenSandbox para sandbox; preserva outro token e demais segredos. Limite 8192 bytes; vazio, máscara, whitespace ou controle retornam 422 validation_error. Retorna 200 com DTO sem segredo; concorrência retorna 409 subaccount_changed, operação em curso retorna 409 subaccount_busy e creating/failed/sem external_id retornam 409 reconciliation_required. Não exige Idempotency-Key. - Atualização local de token exige super_admin, não chama Safe2Pay, não comprova titularidade/validade/habilitação e não cria cobrança. Não muda identidade, ambiente, enabled ou controles de produção. unknown só pode voltar a active quando last_error_code for subaccount_credentials_missing; outros unknown preservam estado/diagnóstico e exigem reconciliação. Cifra e auditoria subaccount.credentials_updated não expõem o segredo. - Diagnóstico: last_error_code interno, last_provider_error_code string numérica de 1 a 10 dígitos ou null, last_error_message estática/sanitizada ou null. error.message pode incluir orientação segura e código; nunca é uma cópia do Error/Message remoto. Código desconhecido não autoriza inferir motivo ou retry. - Código Safe2Pay 301 na criação: chave sandbox da matriz recusada. Configurar chave PRODUÇÃO da mesma matriz, mantendo environment=sandbox para TokenSandbox das cobranças. Só failed definitivo sem external_id permite novo POST deliberado/nova chave após corrigir; creating/unknown exigem reconciliação. - Pagamentos públicos mantêm o mesmo contrato. Não aceitar subaccount_id/provider_id/tokens no corpo; Transaction.subaccount_id é somente leitura, persiste vínculo original para refresh. Sem vínculo: 422 subaccount_not_configured; sem token válido do ambiente: 422 subaccount_credentials_missing. - Só uma rejeição definitiva failed sem external_id permite novo POST deliberado com cadastro corrigido e nova chave. O failed antigo continua consultável e seu replay não faz Add. Creating/unknown/active não permitem essa exceção. - Com vínculos não failed, trocar api_key da matriz ou retirar is_marketplace retorna 409 marketplace_has_subaccounts. Não contornar proteção com alteração direta no banco. ## Arquivos - openapi.json: especificação OpenAPI 3.1 dos endpoints documentados - integration.md: tutorial, contrato e aceite - boleto.md: configuração, payload, limites, ambientes, SDK e diagnóstico de boletos - subaccounts.md: cadastro/importação/reconciliação administrativa, isolamento financeiro, taxas e proteção de tokens - integrations.md: conexões administrativas de e-mail, WhatsApp e MinIO/S3; credenciais, testes explícitos e persistência - website.md: website com backend Go unificado, documentos privados e conversão deliberada em tenant/subconta - operations.md: ciclo de vida, respostas, limites e operação - examples/create-pix.mjs: fetch Node, sem dependências - examples/create_pix.py: Python urllib, sem dependências - examples/create_pix.go: Go net/http, sem dependências - examples/prepare-boleto.mjs: prepara e persiste uma vez o payload de boleto com vencimento válido; nunca sobrescreve arquivo - examples/create-boleto.mjs, create_boleto.py, create_boleto.go: exemplos sem retries, usando o payload e chave persistidos - ../packages/sdk/src/index.ts: SDK TypeScript com fetch injetável e erros tipados Verificação final: executar testes contra simulator local; testar repetição idempotente, alteração de payload, escopo de tenant e falta de credencial. Não afirmar teste financeiro real sem evidência de uma chamada real autorizada. ## Website integrado O website apps/website usa o mesmo backend Go e PostgreSQL. Consulte /docs/website.md e os grupos Website/Cadastros do site do OpenAPI. /website/* exige X-Website-Key de servidor, diferente de X-API-Key de tenant. /admin/website-applications* exige Bearer de super_admin. Cadastro do site não cria tenant/usuário/pagamento automaticamente. Novos documentos privados ficam em MinIO/S3; metadados, hash e vínculo com a conexão original ficam no PostgreSQL. O download passa pela API e exige super admin; nunca expor URL pública, uploadToken ou credencial do storage. ## Conversão administrativa do cadastro - POST /admin/website-applications/{id}/convert exige Bearer super_admin e Idempotency-Key de 1–128 caracteres. Body {tenant:{name,slug,type_id,parent_id?},provider_id,registration}. registration segue SubaccountRegistration, sem tenant/provider/tokens internos. Confirme com o usuário antes de executar cadastro real; ambiente sandbox de pagamentos não simula a criação cadastral. - Tenant e tentativa de subconta são reservados antes de Add remoto. Não cria usuário, API key, regra de pagamento ou cobrança. Não ativa flags de produção. Provider Safe2Pay exige marketplace; gestão usa chave de produção da matriz. - GET summary/detail mostram tenant_id/subaccount_id/provider_id/subaccount_status/subaccount_external_id, conversion_status, conversion_retry_allowed e conversion_error seguro. Detail inclui conversion_tenant original. Status de revisão received/in_review/archived é separado da conversão. - data de conversão contém application, tenant_id, subaccount_id, conversion_status, subaccount_status e error null|{code,message,provider_error_code}. 201 converted; 202 creating ou primeira unknown; 200 failed/replay conhecido (creating replay ainda 202). Ler status: HTTP 200 não equivale a sucesso cadastral. - creating/unknown exigem consulta/reconciliação. Mesma chave/body não faz outro Add. Corpo diferente gera 409 idempotency_conflict; nova tentativa bloqueada gera 409 conversion_already_started. Não descartar tentativa ao receber timeout/5xx. - Só conversion_retry_allowed=true (failed definitivo sem external ID) permite nova chave+registration corrigida. Tenant/provider originais são imutáveis; outra combinação gera 409 conversion_target_immutable. Preservar conversion_tenant e o histórico da tentativa anterior; jamais criar tenant duplicado para contornar o erro. ## Configurações gerais: super admin - GET /admin/integrations/catalog retorna adapters {channel,provider,label,fields:[{key,label,secret,type,required,options?,default?}]}. Use catálogo para campos; não invente providers. Canais email, whatsapp, storage. - GET /admin/integrations é paginado; GET /{id}, POST e PATCH /{id} usam Bearer super_admin. Nenhuma API key pública pode administrar conexões. Body create {name,channel,provider,enabled,is_default,config,credentials}; PATCH preserva config/credentials omitidos. Canal/provider são imutáveis; não há DELETE. - Config não contém segredos. Credenciais cifradas por conexão nunca retornam em DTO: somente has_credentials/configured_secret_fields. No portal, segredo em branco é omitido e preservado. API aceita string vazia para remover, desde que não deixe conexão habilitada sem campo obrigatório. Não colocar tokens em logs, URL, VITE_* ou exemplos versionados. - Adapters: smtp config host/port/from_email/from_name/security(starttls|tls|none), credentials username/password juntos; resend from_email/from_name + api_key; evolution base_url/instance + api_key; meta phone_number_id/api_version + access_token; twilio account_sid/from_number + auth_token; minio/s3 endpoint/bucket/region/use_ssl/force_path_style + access_key/secret_key/session_token opcional. - Várias conexões por canal, no máximo uma padrão habilitada. Desabilitar retira padrão; não escolhe substituta automaticamente. Salvar ou selecionar padrão não envia mensagens. Não há eventos automáticos, campanhas, templates ou webhooks de entrega implementados. - POST /admin/integrations/{id}/test somente por ação explícita. E-mail/WhatsApp exigem recipient e message de até 1.600 caracteres; destinatário WhatsApp com + e DDI. Envia mensagem real uma vez; success=true significa aceite do serviço, não entrega. 502 notification_not_confirmed exige consultar provider/destinatário antes de repetir. Não disparar testes de mensagem automaticamente em QA. - Meta/Twilio texto livre exigem janela de atendimento de 24 horas; templates fora da janela não estão implementados. Documentação primária: https://www.twilio.com/docs/whatsapp/key-concepts . Para storage, body {} apenas verifica bucket, não envia mensagem nem move anexos. - Novos anexos exigem storage padrão ativo, senão 503 storage_not_configured. Bucket privado; download autenticado pela API valida tamanho/hash e usa conexão original. Trocar padrão não move arquivos. Após anexos, destino endpoint/bucket/region/use_ssl/force_path_style imutável: 409 storage_has_documents; rotação de credenciais continua disponível. - Legado bytea é somente leitura até migrate-website-documents copiar/verificar objetos e retirar bytes do banco. Não fazer limpeza manual. Backups precisam PostgreSQL + objetos + ENCRYPTION_KEY. - Compose local fornece API MinIO em 19000 e console em 19001, endpoint interno http://minio:9000 e storage-init. Bootstrap cria bucket privado calebe-documents e conexão somente se não existe nenhuma configuração de storage; não sobrescreve nem reativa conexões existentes.