# Integrar com a Calebe Pay Contrato desta versão: API REST `/v1`, JSON em UTF-8, moeda BRL e valores inteiros em **centavos**. `1250` significa R$ 12,50. A aplicação do cliente conversa com a Calebe Pay; somente o backend da Calebe Pay acessa o provedor. - Especificação para ferramentas e geração de clientes: [`openapi.json`](./openapi.json). - Instruções para agentes de programação: [`llms.txt`](./llms.txt). - Exemplos executáveis: [`examples/`](./examples/). - Operação, estados e limitações: [`operations.md`](./operations.md). - Boleto, endereço, vencimento, sandbox remoto e exemplos: [`boleto.md`](./boleto.md). O passo a passo abaixo demonstra Pix; as consultas e a hierarquia são compartilhadas. - Subcontas de marketplace, apenas para super admin: [`subaccounts.md`](./subaccounts.md). Não há gestão de subcontas pela chave pública do tenant. ## 1. Escolher o ambiente correto **O Pix da Safe2Pay não oferece sandbox**, conforme a [documentação oficial de criar cobrança](https://developers.safe2pay.com.br/reference/cobranca-criar). O parâmetro `IsSandbox` de outros meios de pagamento não torna um Pix simulado. Esta primeira versão tem dois modos distintos: | Modo | Provedor | Dinheiro real | Resultado | | --- | --- | --- | --- | | Desenvolvimento | `simulator`, ambiente `sandbox` | Não | Transação persistida e payload de teste que não pode ser pago | | Integração real | `safe2pay`, ambiente `production` | Sim | Cobrança Pix real; exige credenciais reais, `allow_live_payments` efetivo habilitado e autorização restrita ou opt-in explícito de produção geral | Sempre verifique `simulated` e `environment` na resposta. Não mostre um payload do simulador como um QR Code pagável. Para Pix, `safe2pay` + `sandbox` é recusado com `provider_pix_sandbox_unsupported`. Boleto aceita sandbox Safe2Pay com credencial própria e `simulated=false, environment=sandbox`; isso continua sendo teste, sem confirmação de dinheiro real. O super admin administra o controle global em `GET/PATCH /admin/settings/payments`; chaves de tenant não têm acesso. O valor salvo vale sem reinício e prevalece sobre o padrão `ALLOW_LIVE_PAYMENTS` do ambiente. Os demais limites continuam no servidor; veja [o controle de produção](./operations.md#controle-de-pagamentos-em-produção-no-super-admin). Para um teste real pontual, `LIVE_PAYMENT_SCOPE_JSON` limita tenant, provider, referência, chave idempotente e valor. Produção geral sem escopo exige também `ALLOW_UNSCOPED_LIVE_PAYMENTS=true`. Não amplie esses controles para contornar um erro; veja [o procedimento operacional](./operations.md#teste-real-limitado-a-uma-cobrança). A implementação do conector real não significa que a conta Safe2Pay já esteja habilitada ou que uma cobrança real tenha sido validada. Este projeto não traz credenciais do provedor. ## 2. Receber uma chave de API 1. Um administrador cadastra seu tenant e configura a regra de pagamento/provedor. 2. No painel, ele cria uma chave para esse tenant com `pix:write` e `transactions:read`. 3. Copie o segredo quando ele for criado; as listagens posteriores não recuperam seu valor. 4. Guarde-o como `CALEBE_API_KEY` no secret manager ou nas variáveis do **seu servidor**. Envie `X-API-Key: ` em cada chamada pública. A chave determina o tenant de escrita; **não envie `tenant_id` na criação pública**. Não confunda a chave Calebe Pay do tenant com a chave Safe2Pay, que pertence à configuração interna do provedor. Não inclua chaves de API em `VITE_*`, `EXPO_PUBLIC_*`, localStorage, código React, Electron renderer, repositórios, prompts ou logs. O navegador, o app Expo e o desktop acessam seu backend; o painel administrativo usa uma sessão Bearer com o papel do usuário. ## 3. Criar a primeira transação ```sh export CALEBE_API_URL='http://localhost:18080' # Defina CALEBE_API_KEY no ambiente, sem salvar seu segredo no arquivo. # Esta chave deve ser persistida no pedido ANTES de chamar a API. export CALEBE_IDEMPOTENCY_KEY='pedido-2026-00042-pix-1' curl --fail-with-body "$CALEBE_API_URL/v1/pix" \ -H "X-API-Key: $CALEBE_API_KEY" \ -H "Idempotency-Key: $CALEBE_IDEMPOTENCY_KEY" \ -H 'Content-Type: application/json' \ --data '{ "amount": 1250, "reference": "pedido-2026-00042", "description": "Pedido de demonstração", "customer": { "name": "Cliente de demonstração", "document": "00000000000", "email": "cliente@example.com", "phone": "11999999999", "address": { "zip_code": "01001000", "street": "Rua Exemplo", "number": "100", "district": "Centro", "city": "São Paulo", "state": "SP" } }, "expires_in": 3600 }' ``` Os dados acima são fictícios e servem ao **simulador local**. Para uma cobrança real, use os dados legítimos do pagador e um provedor real habilitado. O cliente não pode escolher livremente o provedor no payload; o roteamento vem das regras administrativas. Campos de entrada: | Campo | Tipo | Regra | | --- | --- | --- | | `amount` | inteiro | 1 a 100.000.000 centavos | | `reference` | string | Referência do seu pedido, obrigatória, até 100 caracteres | | `description` | string | Descrição obrigatória, até 200 caracteres | | `customer.name` | string | Nome do pagador, de 2 a 120 bytes | | `customer.document` | string | 11 ou 14 caracteres alfanuméricos, sem pontuação; preserve como string | | `customer.email` | string | E-mail do pagador, contendo `@`, até 254 bytes | | `customer.phone` | string | Obrigatório para nova cobrança; somente dígitos após trim, até 20 bytes no valor original; sem mínimo de 10/11 dígitos | | `customer.address.zip_code` | string | Obrigatório; CEP com 8 dígitos, sem pontuação | | `customer.address.street` | string | Obrigatório; logradouro com 2 a 120 bytes | | `customer.address.number` | string | Obrigatório; 1 a 20 bytes, aceita `S/N` | | `customer.address.complement` | string | Opcional; até 120 bytes | | `customer.address.district` | string | Obrigatório; bairro com 2 a 80 bytes | | `customer.address.city` | string | Obrigatório; cidade com 2 a 80 bytes | | `customer.address.state` | string | Obrigatório; UF brasileira válida, como `SP` | | `expires_in` | inteiro | Segundos, 60 a 86.400; padrão 3.600 | Telefone e endereço são obrigatórios em **novas cobranças Pix**, inclusive no simulador. A validação de endereço considera trim e UF em maiúsculas em uma cópia, sem alterar os valores desses campos usados no hash idempotente e no snapshot da API. O corpo preparado para a Safe2Pay usa os valores normalizados. Os limites de texto são em bytes UTF-8 após trim, exceto o limite de telefone Pix, que considera o valor original. Não complete um pagador real com os dados fictícios do exemplo. A Calebe Pay monta automaticamente o contrato Safe2Pay: `PaymentMethod: "6"`, `IsSandbox: false`, `Application`, `Products` com o valor em reais e `PaymentObject.Expiration`, além de mapear `customer` para `Customer`, com telefone e endereço. O integrador envia apenas os campos públicos acima; não precisa acrescentar os campos internos do provider. A [referência oficial de criação de cobrança](https://developers.safe2pay.com.br/reference/cobranca-criar) documenta o contrato e informa que Pix não tem sandbox. Um sucesso retorna `{ "data": { ...transação } }`. Salve `data.id` junto ao pedido, ao corpo enviado e à chave de idempotência. `data.reference` é uma correlação comercial; **não substitui** `Idempotency-Key` e não tem garantia de unicidade. Os campos mais importantes da transação são `id`, `tenant_id`, `amount`, `currency`, `status`, `simulated`, `environment`, `pix_copy_paste`, `pix_qr_code`, `expires_at`, `provider_reference` e `subaccount_id`. Os campos de Pix podem ser `null`, especialmente quando o resultado ainda é incerto. Não invente um QR Code quando o provedor não devolvê-lo. Em provider `is_marketplace: true`, o backend exige uma subconta ativa e habilitada do tenant exato; o filho não herda a subconta financeira do pai. `subaccount_id` é o vínculo local gravado na emissão e reutilizado no refresh. Esse campo, `provider_id` e tokens Safe2Pay nunca são aceitos no corpo público da cobrança. A configuração do vínculo é feita pelo super admin antes da integração. A gestão administrativa desse provider usa a chave de produção da matriz. Isso também vale quando o ambiente das cobranças é sandbox: nesse caso, pagamentos usam `TokenSandbox` da subconta. Consulte o [diagnóstico 301 e o procedimento de correção](subaccounts.md#diagnóstico-301-chave-sandbox-da-matriz), sem alterar as proteções de pagamentos reais. ## 4. Idempotência e resultado incerto `POST /v1/pix` exige o header `Idempotency-Key`, com 1 a 128 caracteres: letras ASCII, números, ponto, hífen, dois-pontos ou sublinhado (`^[A-Za-z0-9._:-]{1,128}$`). O escopo é por tenant, e o servidor compara o corpo normalizado. Reenviar a mesma chave e o mesmo pedido reutiliza a transação persistida; mudar os dados usando a mesma chave produz conflito. A chave não expira automaticamente nesta versão. A primeira criação confirmada retorna HTTP `201`. Resultado externo incerto retorna `202` com `status: unknown`. Uma repetição retorna `200` e `Idempotency-Replayed: true`, ou `202` se ainda estiver `processing`. A repetição devolve o registro existente inclusive se ele estiver `failed` ou `unknown`; HTTP 200 não significa que a cobrança foi paga. Uma rejeição conhecida do provedor retorna `502 provider_rejected`, com a tentativa registrada e `error.transaction_id`. Consulte esse ID por GET local. Se o ID não estiver disponível, filtre por `reference` ou repita a chave original para recuperar o registro, sem nova submissão. ### Diagnóstico persistido de Pix e boleto O DTO de transação inclui três campos opcionais em conteúdo, sempre representados como valor ou `null`: | Campo | Contrato | | --- | --- | | `provider_error_code` | String numérica de 1 a 10 dígitos do provider, sanitizada. | | `provider_error_message` | Orientação estática segura da Calebe Pay, até 400 caracteres. Nunca é o `Error`/`Message` bruto do provider. | | `provider_http_status` | HTTP observado no provider, inteiro entre 100 e 599. Pode ser `200` em uma recusa de negócio, embora a Calebe Pay responda `502`. | Em erros de pagamento diagnosticados após a reserva, esses mesmos campos aparecem **diretamente em `error`**, junto de `transaction_id`, `code`, `message` e `request_id`; não há objeto `metadata`. Erros de validação anteriores à reserva podem não ter `transaction_id` nem os diagnósticos. `request_id` identifica a requisição e continua no envelope de erro/auditoria, não na transação. ```json { "error": { "code": "provider_rejected", "message": "O provider recusou a cobrança. Confira a configuração.", "request_id": "req_EXEMPLO", "transaction_id": "txn_EXEMPLO", "provider_error_code": null, "provider_error_message": "O provider recusou a cobrança. Confira a configuração.", "provider_http_status": 200 } } ``` Recusa **confirmada** termina como `failed` e a primeira resposta é `502 provider_rejected`, inclusive quando a Safe2Pay retorna `HasError: false` mas o status da cobrança indica falha. Uma falha **incerta**, como timeout, resposta incompleta ou erro sem desfecho confiável, mantém `unknown` com `202`; os diagnósticos ajudam a investigar, mas não provam que a cobrança não existe. O SDK recebe esse `202` como `Transaction`, não como exceção. `GET /v1/transactions/{id}` lê os diagnósticos já persistidos, sem chamar a Safe2Pay. Repetir o POST com a chave e o corpo originais também não reenvia ao provider: uma tentativa `failed` retorna `200` com o mesmo registro. Não gere uma chave nova apenas para descobrir o erro. Se a consulta ao provider no refresh falhar, seu envelope de erro descreve aquela consulta e inclui o ID local; ele não sobrescreve o status nem o diagnóstico da criação. Os campos nulos não permitem inferir a causa. Para automação, use `code`, estado e IDs, sem analisar trechos de mensagem livre. **Histórico anterior pode ter diagnósticos nulos.** Se o retorno remoto não foi capturado, o banco não consegue reconstruir o motivo original. Um novo envio seria outra operação financeira e não recuperaria aquele erro. A consulta operacional por referência pode localizar uma cobrança existente, mas não promete explicar uma recusa pré-criação; consulte [reconciliação e limites do diagnóstico](operations.md#consulta-safe2pay-por-referência-sem-reenvio). Algoritmo de integração: 1. Crie seu pedido no banco. 2. Gere uma chave única, por exemplo um UUID, e salve-a no pedido. 3. Salve o payload exato que vai enviar. Não altere a expiração ao repetir. 4. Faça a chamada uma vez. 5. Se houver timeout/queda de conexão, preserve a chave e o payload. A chamada pode ter chegado ao servidor. 6. Reenvie a mesma requisição ou consulte o ID conhecido. Nunca crie uma chave nova automaticamente após uma falha de transporte. 7. Para `unknown`, faça reconciliação/consulta e investigue o `request_id`. Não interprete a incerteza como ausência de cobrança. Não há garantia de processamento exatamente uma vez entre Calebe Pay e provedor externo. O registro local e a idempotência reduzem duplicações; resultados incertos exigem reconciliação. O SDK não tenta novamente por conta própria. ### Repetir uma transação Pix antiga A exigência de telefone/endereço não altera transações já registradas. Um replay usa **o corpo e a chave originais**, mesmo quando esse corpo antigo não tinha esses campos; ele devolve o registro existente sem novo envio ao provider. Adicionar endereço ou telefone ao corpo de uma chave já usada altera seu conteúdo e retorna `409 idempotency_conflict`. Uma requisição incompleta com chave ainda não registrada é uma nova tentativa e retorna `422 validation_error`. `createPix` valida o contrato atual antes da rede. Para um replay legado deliberado, use o método de baixo nível, preservando exatamente o pedido salvo no seu sistema: ```ts import { type APIEnvelope, type Transaction } from '@calebe-pay/sdk'; const replay = await pay.request>('/v1/pix', { method: 'POST', body: pedidoAntigo.payloadOriginal, idempotencyKey: pedidoAntigo.chaveOriginal, }); const transaction = replay.data; ``` Não faça esse fallback automaticamente após um erro de validação e não use um snapshot `legacy_partial` para reconstruir o corpo original. Quando o ID é conhecido, prefira o GET local para consultar o resultado existente. ## 5. Consultar e acompanhar o pagamento ```sh # Estado persistido na Calebe Pay: curl --fail-with-body "$CALEBE_API_URL/v1/transactions/SEU_TRANSACTION_ID" \ -H "X-API-Key: $CALEBE_API_KEY" # Solicitar atualização do estado pelo provedor: curl --fail-with-body -X POST \ "$CALEBE_API_URL/v1/transactions/SEU_TRANSACTION_ID/refresh" \ -H "X-API-Key: $CALEBE_API_KEY" ``` O `GET` consulta a base local e exige `transactions:read`. O `POST .../refresh` consulta o conector e persiste o estado atualizado; exige `transactions:read` e `pix:write` para Pix ou `boleto:write` para boleto. Para a experiência de checkout, comece com uma consulta a cada 5 segundos, reduza a frequência depois de um minuto e interrompa a consulta frequente após expiração ou estado final. Esse intervalo é uma sugestão do integrador, não um SLA do servidor. ### Consultar o JSON da requisição `GET /v1/transactions/{id}/request` retorna o conteúdo da criação guardado na Calebe Pay. Exige `transactions:read` e respeita a mesma hierarquia do GET da transação; fora do escopo retorna `404`. O painel usa o alias `GET /admin/transactions/{id}/request` com sessão Bearer. Ambos consultam somente o banco local. ```sh curl --silent --show-error --fail-with-body \ "$CALEBE_API_URL/v1/transactions/SEU_TRANSACTION_ID/request" \ -H "X-API-Key: $CALEBE_API_KEY" ``` O envelope é `{data:{transaction_id,source,request,provider_request,missing_fields}}`: | Campo | Significado | | --- | --- | | `source: "captured"` | Corpos persistidos na reserva da transação, antes da chamada externa; `missing_fields` é vazio. | | `request` | Objeto normalizado pela API, com `tenant_id` resolvido e defaults aplicados. Não é uma cópia byte a byte do JSON original. | | `provider_request` | Corpo preparado para o provider com os mesmos builders da emissão. É `null` no simulador e quando não há captura histórica. | | `source: "legacy_partial"` | Registro anterior à captura: `request` contém apenas os dados ainda conhecidos e `missing_fields` informa o que não pode ser recuperado. | **Um corpo preparado não prova que houve envio, aceite ou pagamento.** Ele pode existir mesmo quando uma proteção ou falha impediu a chamada externa. Leia o estado e o diagnóstico da transação separadamente. O snapshot não devolve headers, tokens ou `Idempotency-Key`, mas contém dados pessoais do pagador e só deve ser exibido a usuários autorizados. Esses corpos não são acrescentados às listagens gerais de transações. No Pix capturado, `expires_in` inclui o valor efetivo, por padrão `3600`. Boleto não inclui `expires_in`; opções com defaults zero/false podem ficar omitidas no JSON normalizado. No histórico parcial de Pix, `missing_fields` inclui `expires_in` e `provider_request`. Para boleto, inclui `boleto.instruction`, `boleto.penalty_rate`, `boleto.interest_rate`, `boleto.cancel_after_due`, `boleto.days_before_cancel` e `provider_request`; inclui também `boleto.due_date` se a data não estiver armazenada. Não deduza opções antigas a partir do prazo atual, não preencha lacunas com defaults e não faça nova emissão para obter um snapshot. No SDK, `await client.getTransactionRequest(id)` retorna diretamente `TransactionRequest`, sem o envelope `data`. O método não altera o estado nem repete a emissão. Continue mantendo o payload e a chave originais no seu sistema para idempotência. Não há webhook de saída para o sistema do tenant nesta versão. Não cadastre uma URL esperando receber eventos que ainda não são enviados. Para produção, a reconciliação dos pedidos pendentes deve continuar no seu backend, inclusive quando o usuário fecha o navegador. | `status` | Significado para sua aplicação | | --- | --- | | `processing` | Registro em processamento; em boleto também pode significar intenção de pagamento ainda sem liquidação | | `pending` | Aguardando pagamento | | `paid` | Pagamento confirmado; autorize entrega apenas pelo estado no backend | | `expired` | Prazo expirado; não autorize entrega | | `failed` | Criação/processamento recusado; examine `failure_code` e os campos `provider_error_*`/`provider_http_status`, quando capturados | | `unknown` | Resultado externo não confirmado; não cobre novamente automaticamente | | `refunded` | Pagamento devolvido, quando informado pelo conector | Há suporte à leitura do estado `refunded`, mas não há endpoint de solicitação de estorno nesta versão. `paid` no simulador ou sandbox representa teste. Confirmação financeira exige `environment=production` e `simulated=false`. ## 6. Hierarquia e listagem Por padrão, `GET /v1/transactions` lista somente as transações do tenant da chave. Uma software house pode incluir descendentes com: ```sh curl --fail-with-body \ "$CALEBE_API_URL/v1/transactions?include_descendants=true&status=pending&limit=50&offset=0" \ -H "X-API-Key: $CALEBE_API_KEY" ``` `tenant_id` filtra um tenant que esteja dentro do escopo autorizado. A opção de descendentes não concede acesso a pais, irmãos ou árvores de outros clientes. Uma chave de um tenant filho não herda o acesso administrativo do pai. Para criar uma cobrança em nome de um filho pela API pública, use uma chave emitida para esse filho. Listagens retornam `{ "data": [...], "meta": { "limit": 50, "offset": 0, "count": 12 } }`. `count` representa a quantidade de itens desta página; não assuma que seja o total geral. Aumente `offset` por `limit` até receber menos itens que o limite. A paginação por offset pode deslocar itens quando novos registros são inseridos; deduplique pelo `id` se construir uma exportação. Use `method=pix` ou `method=boleto` para filtrar o meio de pagamento. O namespace idempotente é compartilhado pelos dois métodos: reutilizar uma chave Pix em boleto gera 409, em vez de emitir outra cobrança. ## 7. Tratar erros Erros da aplicação seguem: ```json { "error": { "code": "validation_error", "message": "Descrição legível do problema", "request_id": "identificador-para-suporte" } } ``` Use `error.code` para decisões de software; a mensagem legível pode mudar. Guarde o `request_id` e o status HTTP, sem registrar segredos ou o documento completo do cliente. Veja a tabela operacional em [`operations.md`](./operations.md). ## 8. SDK TypeScript No monorepo, importe o pacote interno, que exporta TypeScript para Vite/Expo. Ele depende de `fetch`, `Headers`, `AbortController` e `URL`; Node 22+ oferece essas APIs. Um projeto externo pode copiar o pacote ou publicar uma versão privada após seu processo de release. ```ts import { CalebePayClient, CalebePayError } from '@calebe-pay/sdk'; // Código do servidor. Nunca coloque a API key em um bundle de frontend. const pay = new CalebePayClient({ baseUrl: process.env.CALEBE_API_URL ?? 'http://localhost:18080', apiKey: process.env.CALEBE_API_KEY, }); try { const tx = await pay.createPix(payloadSalvoNoPedido, chaveSalvaNoPedido); await seuBanco.salvarTransacao(pedido.id, tx.id); } catch (error) { if (error instanceof CalebePayError) { console.error({ code: error.code, requestId: error.requestId }); } throw error; } ``` O helper `createPix` retorna diretamente a transação, `getTransaction` e `refreshTransaction` também. `listTransactions` mantém `{data, meta}`. `request` retorna o envelope completo para endpoints de painel. O SDK mantém o contrato público; não transforma erro HTTP em sucesso nem converte reais silenciosamente em centavos. ## 9. Critérios de aceite da sua integração - O pedido grava chave idempotente antes da primeira chamada. - Duas chamadas iguais retornam o mesmo `id`. - Mesma chave com `amount` diferente é rejeitada. - A chave de um tenant não acessa uma árvore de outro tenant. - Uma falha de rede não dispara uma segunda cobrança com chave nova. - `simulated: true` aparece claramente como teste na experiência do usuário. - Só um estado `paid` lido pelo servidor libera o pedido. - Segredos e dados pessoais não aparecem em bundle, log ou histórico de chat. - Sua rotina de consulta retoma pedidos pendentes após reiniciar o processo.