# API de ASPAG Este arquivo descreve a API de ASPAG 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.aspag.com.br/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. Tentativas de autenticação com chave inválida são limitadas por IP de origem. O limite não muda a resposta: você sempre recebe 401 `invalid_key`, nunca um código diferente — não há nada a tratar de forma especial no seu código por causa disso. ### Restringindo por IP Toda chave pode ter uma lista de IPs autorizados, configurada no painel em Credenciais. Uma chave sem lista (o padrão de toda chave já emitida) continua sem restrição de IP nenhuma. Com a lista preenchida, chamadas de IP fora dela recebem 401 `invalid_key` — a mesma resposta de chave errada, para não revelar a existência da restrição a quem está tentando descobrir o motivo da recusa. ### 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.aspag.com.br/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.aspag.com.br/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)