# API de CREDMAISPAY Este arquivo descreve a API de CREDMAISPAY para uso por assistentes de programação. Contém tudo que é preciso para integrar: autenticação, endpoints, webhooks e os erros possíveis. URL base: https://bank.credmaispay.com/api ## Como usar este documento Você é um assistente ajudando alguém a integrar pagamentos Pix. Leia tudo antes de escrever código. Os pontos marcados com ATENÇÃO são erros que quebram a integração em produção — respeite-os literalmente. --- ## 1. Autenticação Toda chamada leva a chave no header Authorization: Authorization: Bearer sk_live_xxxxx A chave identifica a conta. Não existe parâmetro de conta em nenhuma rota, e uma chave nunca alcança dados de outra. ATENÇÃO: hoje NÃO existe ambiente de sandbox. Os dois prefixos — `sk_test_` e `sk_live_` — operam sobre a conta REAL e movem dinheiro DE VERDADE. O prefixo é apenas um rótulo para você organizar suas chaves; ele não isola valores nem simula pagamentos. Para testar sem risco, use valores pequenos na própria conta. Trate qualquer chave como capaz de mover o dinheiro real da conta. ATENÇÃO: a chave é uma senha. Guarde no servidor. Nunca no código do navegador, nunca no aplicativo, nunca em repositório. Quem tem a chave move o dinheiro da conta. ### Escopos Cada chave carrega apenas os escopos que recebeu na criação: - `balance:read` — consultar saldo - `transactions:read` — listar movimentações e ver comprovante - `transfers:write` — transferir entre contas da plataforma - `pix:write` — criar cobrança Pix - `pix:read` — consultar cobrança Pix - `pix:send` — enviar Pix para uma chave externa (saída de dinheiro) Chamar uma rota sem o escopo devolve 403 `insufficient_scope`. Para um cardápio ou loja que só precisa cobrar e confirmar, os dois escopos suficientes são `pix:write` e `pix:read`. --- ## 2. Primeira chamada: confirme a chave Antes de qualquer coisa, verifique se a chave responde: curl https://bank.credmaispay.com/api/v1/me \ -H "Authorization: Bearer sk_test_sua_chave" Resposta: { "environment": "test", "scopes": ["pix:write", "pix:read"], "account": { "id": "e3fe4b3f-..." }, "tenant": { "slug": "paguemais" }, "permissions": { "balance": true, "transactions": true, "transfers": true } } --- ## 3. Endpoints ### GET /v1/me Escopo: nenhum. Confirma a chave e mostra os escopos dela. ### GET /v1/balance Escopo: `balance:read` { "available": "2.99", "held": "0.00", "gross": "2.99", "currency": "BRL" } ### GET /v1/transactions Escopo: `transactions:read` Parâmetros: `limit` (padrão 25, máximo 100), `offset` (padrão 0) { "items": [ { "id": "6dbda7b0-...", "type": "CHARGE_RECEIVED", "status": "PENDING", "direction": "IN", "amount": "49.90", "fee": "0.00", "description": "Pedido #1234", "counterparty": "Maria Silva", "createdAt": "2026-09-10T15:00:14.993Z", "settledAt": null } ], "total": 128, "limit": 25, "offset": 0 } Pagine com `offset`: ainda há página seguinte enquanto `offset + items.length < total`. ### GET /v1/transactions/{id} Escopo: `transactions:read`. Comprovante de uma movimentação. { "id": "9a29cbf2-...", "type": "CHARGE_RECEIVED", "status": "PAID", "direction": "IN", "amount": "100.00", "fee": "15.00", "total": "85.00", "description": "Pedido #1234", "counterparty": { "name": "Maria Silva", "document": null }, "createdAt": "2026-09-10T17:34:30.579Z", "settledAt": "2026-09-10T17:34:30.601Z", "correlationId": "57befe60-...", "feeBreakdown": { "total": "15.00" }, "timeline": [ { "status": "PAID", "source": "PROVIDER_WEBHOOK", "reason": null, "at": "2026-09-10T17:34:30.601Z" } ] } - `total`: o que esta operação moveu de fato na conta — para quem recebeu, o líquido depois da tarifa; para quem enviou, o valor mais a tarifa que pagou. - `fee` / `feeBreakdown.total`: a tarifa desta operação. Só o total — a composição interna da tarifa não é exposta por esta rota. - `feeBreakdown` vem `null` quando não há tarifa nesta operação. - `timeline`: os eventos que levaram ao status atual, do mais antigo ao mais recente. ### POST /v1/pix/charges Escopo: `pix:write`. Cobrança avulsa: um QR para UM pagamento. ATENÇÃO: isto NÃO cria link de pagamento. A cobrança nasce, é paga e acaba — não tem página pública nem endereço para divulgar. Se o que você quer é um endereço que várias pessoas possam pagar, use POST /v1/payment-links, descrito adiante. Corpo: { "amount": "49.90", "description": "Pedido #1234", "expiresIn": 3600, "payer": { "name": "Maria Silva", "email": "maria@exemplo.com", "document": "12345678901" }, "externalReference": "pedido-1234" } - `amount`: string decimal com duas casas. Obrigatório. - `description`: até 140 caracteres. Opcional — sem ela, o extrato mostra "Cobrança Pix". - `expiresIn`: segundos, de 60 a 2592000. Padrão 86400 (24h). - `payer`: opcional, todos os campos opcionais. - `externalReference`: seu id do pedido, até 120 caracteres. Volta na resposta. Opcional, mas use — é como você liga a cobrança ao pedido no seu sistema. Envie `Idempotency-Key` (8 a 128 caracteres `[A-Za-z0-9_.:-]`) para poder repetir a chamada com segurança: reenviar a mesma chave devolve a cobrança já criada em vez de gerar outra. Sem a chave, cada chamada cria uma cobrança nova — é assim que uma tela de checkout sem esse cuidado costuma duplicar cobrança quando o cliente clica duas vezes ou a rede treme. Resposta 201: { "id": "e0dfecf5-af2f-40e5-9729-2e25b9a4184a", "status": "open", "amount": "49.90", "description": "Pedido #1234", "qrCode": "00020101021226900014br.gov.bcb.pix...", "copyPaste": "00020101021226900014br.gov.bcb.pix...", "expiresAt": "2026-09-10T15:30:10.457Z", "externalReference": "pedido-1234" } `qrCode` e `copyPaste` são o MESMO valor: o código copia-e-cola. Para mostrar o QR, gere a imagem a partir dessa string com qualquer biblioteca de QR code. A API não devolve imagem. ### GET /v1/pix/charges/{id} Escopo: `pix:read`. O estado atual da cobrança. { "id": "e0dfecf5-...", "status": "open", "amount": "49.90", "description": "Pedido #1234", "paidAt": null, "expiresAt": "2026-09-10T15:30:10.457Z" } Estados possíveis: - `open` — criada, aguardando pagamento - `paid` — paga e creditada. É o ÚNICO estado que significa dinheiro na conta - `expired` — passou da validade sem pagamento - `cancelled` — cancelada - `refunded` — estornada ATENÇÃO: só `paid` libera o pedido. Qualquer outro estado, inclusive `open`, significa que o dinheiro não entrou. ### POST /v1/pix/payouts Escopo: `pix:send`. Envia Pix para uma chave externa (Pix de SAÍDA). Diferente de POST /v1/transfers (que move entre contas DESTE banco): aqui o dinheiro sai para uma chave Pix de qualquer instituição. Debita a sua conta. ATENÇÃO: exige o header `Idempotency-Key` (8 a 128 caracteres de [A-Za-z0-9_.:-]). Sem ele a resposta é 400 `missing_idempotency_key`. Reenviar a mesma chave devolve o envio já feito em vez de pagar de novo — é o que protege contra clique duplo e retry de rede num pagamento. Corpo: { "amount": "150.00", "pixKey": "maria@exemplo.com", "pixKeyType": "EMAIL", "description": "Pagamento fornecedor" } - `amount`: string decimal com duas casas. Obrigatório. - `pixKey`: a chave Pix do destino. Obrigatório. - `pixKeyType`: um de `CPF`, `CNPJ`, `EMAIL`, `PHONE`, `RANDOM`. - `description`: até 140 caracteres. Opcional. Resposta 201: { "id": "8f1a...", "status": "processing", "amount": "150.00", "fee": "1.20", "total": "151.20", "pixKey": "ma****@exemplo.com", "providerReference": "E1890...", "endToEndId": "E1890..." } - `amount` é o valor enviado; `fee` a tarifa; `total` o que saiu da conta (amount + fee). `pixKey` volta mascarada. - `status` reflete o estado no provedor no momento do envio (`processing`, `completed`…). O desfecho final também chega pelo webhook. ATENÇÃO: sem saldo suficiente para `amount` + `fee`, a resposta é 400 `insufficient_funds` e nada é enviado. Se a conta não tem Pix de saída habilitado, é 400 `no_provider`. ### POST /v1/payment-links Escopo: `pix:write`. Cria um link de pagamento. Uma página hospedada que fica aberta e recebe de várias pessoas, até ser cancelada ou expirar. Diferente da cobrança avulsa acima. Quando usar cada um: - **Cobrança avulsa** (`/v1/pix/charges`): o pedido já existe no seu sistema e você só precisa do QR. Um pagamento, um QR. - **Link** (`/v1/payment-links`): o valor vai ser divulgado — uma vaquinha, uma mensalidade, um catálogo — e várias pessoas pagam no mesmo endereço. Corpo: { "description": "Mensalidade de setembro", "amount": "99.90", "amountOpen": false, "singleUse": false, "expiresInHours": 720, "requirePayerName": true } - `description`: 2 a 140 caracteres. Obrigatório. - `amount`: string decimal. Omita junto com `amountOpen: true` para quem paga escolher o valor. - `amountOpen`: quando true, quem paga define o valor, dentro de `minAmount` e `maxAmount` se informados. - `singleUse`: true fecha o link no primeiro pagamento. Padrão false. - `maxUses`: fecha depois de N pagamentos. - `expiresInHours`: 1 a 8760 (um ano). - `requirePayerName`, `requirePayerDocument`, `requirePayerEmail`: exigem o dado de quem paga antes de gerar o Pix. Resposta 201: { "id": "4f21c8de-...", "slug": "k3n8vq2p", "url": "https://seu-banco.com/pagar/k3n8vq2p", "description": "Mensalidade de setembro", "amount": "99.90", "amountOpen": false, "singleUse": false, "status": "open" } `url` é o endereço que você manda para quem vai pagar. ### GET /v1/payment-links Escopo: `pix:read`. Os links da conta, com quanto cada um recebeu. { "items": [ { "id": "4f21c8de-...", "slug": "k3n8vq2p", "url": "https://seu-banco.com/pagar/k3n8vq2p", "description": "Mensalidade de setembro", "amount": "99.90", "singleUse": false, "uses": 12, "status": "open", "received": "1198.80", "payments": 12 } ] } ### POST /v1/payment-links/{id}/cancel Escopo: `pix:write`. Fecha o link. Quem abrir depois vê que não está mais disponível. Os pagamentos já recebidos continuam na conta. ### POST /v1/transfers Escopo: `transfers:write`. Transfere entre contas da plataforma. ATENÇÃO: exige o header `Idempotency-Key` (8 a 128 caracteres de [A-Za-z0-9_.:-]). Sem ele a resposta é 400 `missing_idempotency_key`. curl -X POST https://bank.credmaispay.com/api/v1/transfers \ -H "Authorization: Bearer sk_live_sua_chave" \ -H "Idempotency-Key: pedido-8842" \ -H "Content-Type: application/json" \ -d '{"amount":"150.00","to":"@apelido","description":"Pagamento"}' - `to`: aceita `@apelido`, `apelido` ou o UUID da conta. --- ## 4. Idempotência Use `Idempotency-Key` em toda operação que move dinheiro. Reenviar a mesma chave — mesma conta, mesmo valor do header — devolve o resultado da primeira chamada em vez de executar de novo. É o que protege contra timeout e clique duplo: sem isso, um app que reenvia a requisição por segurança quando não recebe resposta a tempo pode gerar duas cobranças ou duas transferências para o mesmo pedido. Obrigatório em `POST /v1/transfers` — a chamada sem o header é recusada. Opcional, mas fortemente recomendado, em `POST /v1/pix/charges`. A chave vale por conta: duas contas podem usar o mesmo valor de `Idempotency-Key` sem conflito entre si. --- ## 5. Webhooks — confirmação de pagamento Cadastre a URL no painel, em Credenciais. O sistema avisa quando o pagamento acontece, em vez de você perguntar de tempos em tempos. ### Eventos - `charge.paid` — a cobrança foi paga e o valor entrou na conta. É ESTE que autoriza liberar o pedido. Vale para os dois caminhos: cobrança avulsa e pagamento feito num link. - `charge.expired` — venceu sem pagamento. Serve para cancelar o pedido em aberto. - `transfer.completed` — uma transferência enviada chegou ao destino. - `credit.drawn` — alguém usou o limite de crédito. - `loan.approved` — um empréstimo foi aprovado. - `investment.applied` — um aporte foi aplicado. - `consortium.quota.approved` — uma cota de consórcio foi aprovada. - `consortium.contemplated` — uma cota foi contemplada. Para loja ou cardápio, assine apenas `charge.paid` e, se quiser cancelar pedidos sozinho, `charge.expired`. ### O que chega POST https://seu-sistema.com/webhooks content-type: application/json x-webhook-id: 9f2c1a44-... x-webhook-event: charge.paid x-webhook-timestamp: 1789002842 x-webhook-signature: 7b52009b64fd0a2a49e6d8a939753077792b0554... { "id": "9f2c1a44-...", "type": "charge.paid", "createdAt": "2026-09-09T21:14:02.000Z", "data": { "chargeId": "e636775c-...", "sellerId": "c34b87db-...", "amount": "4990" } } ATENÇÃO: `data.amount` vem em CENTAVOS (4990 = R$ 49,90), diferente do resto da API, que usa string decimal. `data.chargeId` é o mesmo `id` devolvido na criação. ### Conferindo a assinatura A assinatura é o HMAC-SHA256 de `{timestamp}.{corpo}` em hexadecimal, com o segredo do destino. O segredo aparece ao cadastrar a URL e pode ser recuperado depois no painel, em Credenciais, no botão "Ver segredo" ao lado do destino. import { createHmac, timingSafeEqual } from "node:crypto"; function confere(corpoBruto, headers, segredo) { const assinatura = headers["x-webhook-signature"]; const timestamp = Number(headers["x-webhook-timestamp"]); // Entrega velha é entrega repetida: recuse. if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false; const esperado = createHmac("sha256", segredo) .update(`${timestamp}.${corpoBruto}`) .digest("hex"); const a = Buffer.from(assinatura); const b = Buffer.from(esperado); return a.length === b.length && timingSafeEqual(a, b); } ATENÇÃO: use o corpo CRU, exatamente como chegou. Se você deixar o framework fazer o parse do JSON e depois reserializar, os bytes mudam e a assinatura nunca bate. No Express, use `express.raw({ type: "application/json" })` nessa rota. No Next.js App Router, use `await request.text()`. Este é o erro mais comum de todos: o webhook chega, a assinatura falha, e o pagamento nunca confirma. ### Regras obrigatórias 1. Responda 200 rápido. Processe depois, em fila. Demorar faz o envio ser considerado falho e reenviado. 2. Espere repetição. O mesmo evento pode chegar duas vezes. Guarde o `x-webhook-id` já processado e ignore repetidos, ou o pedido é liberado duas vezes. 3. Não confie no valor recebido para creditar. Antes de liberar o pedido, confirme com `GET /v1/pix/charges/{id}`. O webhook diz o que olhar; a consulta diz o que é verdade. 4. Use HTTPS. URLs em HTTP não são aceitas. 5. Vinte falhas seguidas desativam o destino. Depois de corrigir seu servidor, reative no painel em Credenciais, botão "Reativar" — o segredo continua o mesmo. --- ## 6. Erros A maioria vem como JSON com `error` (um slug estável, para checar por código) e `message` (texto para log ou depuração, não confie no texto em si — ele pode mudar): 401 missing_credentials Faltou o header Authorization 401 invalid_key Chave inválida, expirada ou revogada 403 insufficient_scope A chave não tem o escopo da rota 403 wrong_host Chave usada no domínio de outro banco 403 key_without_account Chave sem conta associada, não opera dinheiro 400 missing_idempotency_key POST /v1/transfers exige Idempotency-Key 400 invalid_idempotency_key Idempotency-Key fora do formato aceito 400 invalid_request Corpo inválido; veja "details" quando vier 404 not_found POST /v1/payment-links/{id}/cancel: link não existe ATENÇÃO: nem todo 400/404 segue esse formato. Um valor abaixo do mínimo, uma cobrança ou link que não existe, e a maioria das validações de negócio (`POST /v1/pix/charges`, `POST /v1/payment-links`) hoje devolvem o formato padrão do framework: { "statusCode": 400, "error": "Bad Request", "message": "Valor abaixo do mínimo por cobrança: mínimo R$ 5,00" } { "statusCode": 404, "error": "Not Found", "message": "Cobranca nao encontrada" } Nesses casos, `error` é só a categoria HTTP ("Bad Request", "Not Found") — quem for tratar o erro por código deve usar o `statusCode` e ler `message` como texto para mostrar ou logar, não comparar contra um valor fixo. `POST /v1/transfers` é exceção: os erros de negócio saem com `error` estável e minúsculo, vindo direto do código — 400 seller_not_found Destino não existe nesta conta 400 seller_inactive Destino existe mas está inativo 400 same_seller Origem e destino são a mesma conta 400 insufficient_funds Saldo insuficiente para a transferência 400 amount_invalid Valor não passa nas regras de negócio 400 limit_exceeded Excedeu limite de valor ou de operações 400 cross_tenant Destino não pertence a este banco `POST /v1/pix/payouts` também sai com `error` estável e minúsculo: 400 insufficient_funds Saldo insuficiente para o valor mais a tarifa 400 no_provider A conta não tem Pix de saída habilitado 400 provider_rejected O provedor recusou o envio; nada saiu 400 timeout_ambiguous Sem resposta do provedor a tempo; conciliar antes de reenviar Não existe hoje um código equivalente para saldo insuficiente em `POST /v1/pix/charges` (que só recebe, nunca debita a própria conta) nem limite de taxa de chamadas (rate limiting) na API. --- ## 7. Formatos - Dinheiro: string decimal com duas casas, `"150.00"`. NUNCA número — ponto flutuante perde centavo. A exceção é `data.amount` no webhook, que vem em centavos como string. - Datas: ISO 8601 em UTC, `"2026-09-06T04:12:00.000Z"`. - Identificadores: UUID. Não presuma ordem nem sequência. --- ## 8. Fluxo completo de uma loja 1. Cliente fecha o pedido no seu sistema. 2. Você chama `POST /v1/pix/charges` com o valor e o `externalReference` do pedido. 3. Guarda o `id` retornado junto do pedido no seu banco. 4. Mostra o `copyPaste` para o cliente, e o QR gerado a partir dele. 5. O cliente paga. 6. Seu endpoint de webhook recebe `charge.paid`. 7. Você confere a assinatura. 8. Você chama `GET /v1/pix/charges/{id}` e confirma `status: "paid"`. 9. Só então libera o pedido. O passo 8 não é opcional. É o que separa "recebi um aviso" de "o dinheiro está na conta". ### Quando o caminho é o link Se o valor é divulgado e várias pessoas pagam no mesmo endereço — uma mensalidade, uma vaquinha —, crie um link com POST /v1/payment-links e divulgue a `url`. Cada pagamento gera seu próprio `charge.paid`, e os passos 6 a 9 valem igual. A diferença é que o link continua aberto depois: um pagamento não o fecha, a menos que `singleUse` seja true. --- ## Checklist antes de ir para produção - [ ] A chave está no servidor, fora do repositório - [ ] O webhook usa o corpo cru para conferir a assinatura - [ ] O webhook responde 200 antes de processar - [ ] Eventos repetidos são ignorados pelo `x-webhook-id` - [ ] O pedido só é liberado após `GET` confirmar `status: "paid"` - [ ] Transferências e envios de Pix (payouts) enviam `Idempotency-Key` (obrigatório) - [ ] Cobranças Pix enviam `Idempotency-Key` (recomendado, evita duplicar) - [ ] Valores tratados como string decimal, nunca float - [ ] Ciente de que sk_test_ e sk_live_ movem dinheiro real (não há sandbox)