# Nimbuu Pay > Gateway de pagamento brasileiro dev-first, feito pra quem constrói com IA. PIX, boleto e cartão pela mesma API, mais links de pagamento, produtos, assinaturas, webhooks assinados e um MCP server pra que seu agente integre sozinho. Comece em **sandbox** (pagamento simulado, zero dinheiro real); quando estiver pronto, troque a chave `sk_test_` por `sk_live_` e o código não muda. ## Conceitos - **Base URL:** `https://api.nimbuupay.com` - **Auth:** header `Authorization: Bearer `. NUNCA exponha a chave no front-end — ela dá acesso total à conta; chame sempre do seu backend. - **Valores em centavos:** `amount: 4990` = R$ 49,90. Moeda sempre `BRL`. Mínimo 100. - **Modos:** `sk_test_` (sandbox, simulado) e `sk_live_` (produção). Mesma API, muda só a chave. A `sk_live_` só é liberada após aprovação do cadastro (KYC) no painel. - **Charge (cobrança):** uma cobrança avulsa. Você cria, mostra o meio de pagamento pro cliente, e a confirmação chega por webhook. - **Checkout link:** checkout hospedado, sem você escrever tela. URL fica na RAIZ: `https://nimbuupay.com/`. - **Product / Price:** o que você vende e quanto custa. Um produto pode ter vários preços (mensal, anual). - **Subscription:** recorrência. Duas portas: `POST /v1/subscriptions` (você tem os dados do cliente) ou o checkout de um produto com `recurring_interval` (você não escreve tela). As duas geram os mesmos eventos. - **Refund (estorno):** devolve dinheiro de uma cobrança paga, total ou parcialmente. Parcial NÃO muda o status: a cobrança segue `paid` com `amount_refunded > 0`. - **Paginação:** toda listagem devolve no máximo 100 itens, com `has_more` e `next_cursor`. Cursor, não offset. - **Limite de taxa:** 300 req/min por chave, das quais 60/min de escrita. ## Superfície da API (autenticada por chave) | recurso | rotas | |---|---| | Cobranças | `POST /v1/charges` · `GET /v1/charges` · `GET /v1/charges/{id}` · `POST /v1/charges/{id}/pay` (só sandbox) | | Estornos | `POST /v1/charges/{id}/refund` · `GET /v1/charges/{id}/refunds` | | Webhooks | `POST /v1/webhooks` · `GET /v1/webhooks` · `DELETE /v1/webhooks/{id}` | | Produtos | `POST /v1/products` · `GET /v1/products` · `GET /v1/products/{id}` · `PATCH /v1/products/{id}` · `POST /v1/products/{id}/archive` | | Assinaturas | `POST /v1/subscriptions` · `GET /v1/subscriptions` · `GET /v1/subscriptions/{id}` · `POST /v1/subscriptions/{id}/cancel` | | Links | `POST /v1/checkout-links` · `GET /v1/checkout-links` | ## Criar cobrança `POST /v1/charges` ```json { "amount": 4990, "description": "Pedido #123", "method": "pix", "customer": { "name": "Cliente", "document": "12345678909", "email": "cliente@email.com" }, "expiresIn": 3600, "metadata": { "pedido_id": "123" } } ``` - Obrigatórios: `amount` (int, centavos, mín. 100), `customer.name`, `customer.document` (CPF/CNPJ, dígito verificador é validado). - `description` até 140 chars. `method`: `pix` (padrão) | `boleto` | `credit_card`. - **Sempre mande `Idempotency-Key: `.** Mesma key → devolve a cobrança original em vez de criar outra. É o que evita cobrar o cliente duas vezes quando a rede cai e o código tenta de novo. Response (201): ```json { "id": "ch_test_...", "object": "charge", "status": "pending", "amount": 4990, "currency": "BRL", "method": "pix", "pix": { "qr_code": "00020126...", "qr_code_base64": "data:image/png;base64,...", "expires_at": "2026-..." }, "paid_at": null } ``` Status: `pending` · `paid` · `expired` · `failed` · `canceled` · `refunded`. `refunded` = devolvida por INTEIRO. Estorno parcial mantém `paid` e sobe `amount_refunded`. ## Métodos: PIX, boleto, cartão Mesma rota, muda o `method` e o que volta. - **PIX** (padrão): volta `pix.qr_code` (copia-e-cola) e `pix.qr_code_base64` (imagem). Confirma em segundos. - **Boleto**: volta `boleto.url`, `boleto.pdf` e `boleto.expiration`. Compensa em 1-3 dias úteis — NÃO libere o produto na criação, libere no `charge.paid`. - **Cartão**: exige `card` e aceita `installments` de 1 a 12. ```json { "amount": 4990, "method": "credit_card", "installments": 3, "customer": { "name": "Ana", "document": "12345678909" }, "card": { "number": "4111111111111111", "holder_name": "ANA SOUZA", "exp_month": 12, "exp_year": 2030, "cvv": "123" } } ``` Não guarde número de cartão no seu banco: é risco e joga você no escopo pesado de PCI sem necessidade. Cartão pode voltar `failed` na hora, diferente do PIX. ## Sandbox: simular pagamento `POST /v1/charges/{id}/pay` marca como paga e **dispara o webhook de verdade**. É assim que se testa o fluxo inteiro sem mover dinheiro. Só funciona com `sk_test_`. ## Estornos `POST /v1/charges/{id}/refund` — sem `amount`, devolve tudo que ainda resta. ```json { "amount": 1000, "reason": "Produto devolvido" } ``` Response: `{ "id": "re_live_...", "object": "refund", "charge_id": "ch_live_...", "amount": 1000, "status": "succeeded" }` - Parcial NÃO vira total: a cobrança acumula `amount_refunded` e só vira `refunded` quando tudo volta. - Os dois casos disparam `charge.refunded`. **Compare `amount_refunded` com `amount`** antes de revogar acesso — tratar todo `charge.refunded` como cancelamento corta o acesso de quem só pediu R$ 10 de volta. - Mande `Idempotency-Key`: mesma key devolve o mesmo estorno, nunca um segundo. - **Sai do saldo o valor DEVOLVIDO, não o líquido recebido.** Venda de R$100 credita R$99,01 (taxa R$0,99); devolver R$100 tira R$100. A taxa não volta porque é o custo que o banco cobrou na entrada e ele não devolve. Mesmo modelo de Stripe e maquininha. - **Saldo insuficiente NÃO impede o estorno**: ele passa e o saldo fica negativo. Segurar a devolução geraria chargeback no cartão e reclamação no PIX, mais caro que o negativo. Com saldo negativo o SAQUE fica bloqueado até as próximas vendas quitarem. Só acima de um teto de exposição a API recusa (`400 refund_exposure_exceeded`). - O estorno só é registrado depois que o dinheiro sai de verdade. Provedor recusou → a cobrança fica intacta. - Erros: `400 charge_not_refundable`, `400 already_refunded`, `400 refund_exceeds_charge`, `400 refund_exposure_exceeded`. - `GET /v1/charges/{id}/refunds` lista o histórico (uma cobrança pode ter vários parciais). ## Paginação Toda listagem (`/v1/charges`, `/v1/subscriptions`, `/v1/products`, `/v1/checkout-links`, `/v1/webhooks`) devolve: ```json { "object": "list", "data": [], "has_more": true, "next_cursor": "ch_live_..." } ``` `GET /v1/charges?limit=50&starting_after=` - `limit` de 1 a 100, padrão 100. Fora da faixa é `400 invalid_limit`, não corte silencioso. - `starting_after` inválido/de outra conta é `400 invalid_cursor`. - Cursor aponta pra um REGISTRO, não pra uma posição: venda nova entrando no meio da varredura não faz você ler a mesma duas vezes nem pular nenhuma. É por isso que não existe `?page=`. - No SDK: `for await (const c of nimbuu.charges.listAll()) {}`. - Ainda assim, pra saber de pagamento reaja a webhook. Varredura é pra conciliação, não pra descobrir venda. ## Limite de taxa Por chave de API, janela de 1 minuto: - **300 req/min** — todas as requisições autenticadas. - **60 req/min** — só escrita (POST/PATCH/PUT/DELETE). Sub-teto do de cima. É por CHAVE, não por IP. Toda resposta traz `RateLimit-Limit`, `RateLimit-Remaining` e `RateLimit-Reset` (segundos). Estourou → `429 rate_limited` com `Retry-After`: espere o que ele mandar, repetir na hora só queima a janela seguinte. O checkout público tem proteção própria e mais rígida, que não conta contra estes limites. ## Links de pagamento `POST /v1/checkout-links` — checkout hospedado, sem escrever tela. ```json { "product_id": "prod_...", "label": "Campanha", "success_url": "https://seuapp.com/obrigado", "allow_coupons": true } ``` Response: `{ "id": "lnk_...", "slug": "052bb5bb1a", "url": "https://nimbuupay.com/052bb5bb1a" }` Campos: `product_id` ou `products[]` (vários, com seletor), `price_id`, `label` (interno), `success_url`, `return_url`, `allow_coupons`, `require_address`. Links antigos no formato `/l/` seguem funcionando e redirecionam sozinhos. ## Produtos `POST /v1/products` ```json { "name": "Curso de Programação", "price": { "amount": 19700 }, "payment_methods": ["pix", "credit_card"] } ``` Campos: `name` (obrigatório), `description`, `price.amount` (centavos), `price.amount_type` (`fixed` | `custom` | `free`), `prices[]`, `recurring_interval` (`month` | `year` → vira assinatura), `trial_days`, `setup_fee`, `payment_methods`, `visibility` (`public` | `private`), `metadata`. `GET /v1/products` aceita `?status=`, `?q=`, `?sort=name|recent`. Arquivar (`POST /v1/products/{id}/archive`) tira das listas sem apagar: não existe deletar, porque apagar produto quebraria o histórico financeiro. ## Assinaturas Intervalos: `monthly`, `yearly`. Duas portas de criação, mesmos eventos. `POST /v1/subscriptions` — direto pela API: ```json { "interval": "monthly", "value": 4990, "method": "credit_card", "trial_days": 7, "customer": { "name": "Ana", "document": "12345678909", "email": "ana@ex.com", "phone": "11999999999" }, "card": { "number": "4111111111111111", "holder_name": "ANA SOUZA", "exp_month": 12, "exp_year": 2030, "cvv": "123" } } ``` - `method: credit_card` = auto-débito, 1ª cobrança na hora. `pix`/`boleto` = uma cobrança por ciclo, e a primeira nasce PENDENTE. - A resposta traz `first_charge_id`: em pix/boleto, consulte `GET /v1/charges/{first_charge_id}` pra pegar o QR ou a linha digitável. - `trial_days` só vale no cartão. Anti-abuso embutido: CPF/CNPJ que já usou teste nesta conta passa a ser cobrado desde o 1º ciclo. Ou o cliente paga um produto com `recurring_interval` no checkout hospedado. `GET /v1/subscriptions` (paginado) · `GET /v1/subscriptions/{id}` · `POST /v1/subscriptions/{id}/cancel` (vale pras cobranças futuras, não estorna o que já foi pago — pra devolver dinheiro, use estorno). ## Webhooks `POST /v1/webhooks` com `{ "url": "https://seuapp.com/webhooks/nimbuu", "events": ["charge.paid"] }` O `secret` só aparece UMA VEZ, na criação. Guarde na hora. Eventos: `charge.paid` · `charge.failed` · `charge.expired` · `charge.canceled` · `charge.refunded` · `subscription.created` · `subscription.canceled`. `charge.refunded` cobre estorno TOTAL e PARCIAL — compare `amount_refunded` com `amount` no payload antes de revogar acesso. ### Validar a assinatura (importante) Header `Nimbuu-Signature: t=,v1=`. Valide `HMAC_SHA256(secret, ".")` em tempo constante, usando o **corpo cru** — se você validar o JSON já parseado e re-serializado, a assinatura não bate. O `t` serve pra rejeitar replay. Sem essa validação, qualquer um que descubra sua URL manda um `charge.paid` falso e libera produto sem pagar. ```ts import { verifyWebhook } from "nimbuu-pay"; const ok = verifyWebhook(rawBody, req.headers["nimbuu-signature"], process.env.NIMBUU_WEBHOOK_SECRET!); if (!ok) return res.status(400).end(); ``` Entrega com falha é reenviada com backoff. Responda `2xx` rápido e processe depois — se demorar, você recebe o mesmo evento de novo, então trate o handler como idempotente. ## SDK TypeScript ```ts npm i nimbuu-pay import { NimbuuPay } from "nimbuu-pay"; const nimbuu = new NimbuuPay(process.env.NIMBUU_API_KEY!); const charge = await nimbuu.charges.create({ amount: 4990, customer: { name: "Ana", document: "12345678909" } }); console.log(charge.pix.qr_code); ``` ## MCP (pro seu agente de IA integrar sozinho) ```json { "mcpServers": { "nimbuu-pay": { "command": "npx", "args": ["-y", "nimbuu-pay-mcp"], "env": { "NIMBUU_API_KEY": "sk_test_..." } } } } ``` Tools: `nimbuu_create_charge`, `nimbuu_get_charge`, `nimbuu_list_charges`, `nimbuu_refund_charge`, `nimbuu_create_subscription`, `nimbuu_simulate_payment`, `nimbuu_quickstart`. ## Erros Sempre JSON: `{ "error": { "type", "code", "message" } }` - `401 no_api_key` / `401 invalid_api_key` — autenticação - `400 invalid_request` — payload inválido (amount < 100, CPF/CNPJ com dígito errado, e-mail malformado) - `400 insufficient_balance` — saldo insuficiente - `400 invalid_limit` / `400 invalid_cursor` — paginação - `400 charge_not_refundable` / `400 already_refunded` / `400 refund_exceeds_charge` / `400 refund_exposure_exceeded` — estorno - `404 charge_not_found` / `webhook_not_found` / `product_not_found` - `429 rate_limited` — estourou o limite da chave; vem com `Retry-After` - `429 too_many_attempts` — vem com header `Retry-After` ## Limites e o que ainda NÃO existe Dito na cara pra você não descobrir em produção: - **Assinatura não muda de plano no meio do caminho.** Não há upgrade/downgrade com valor proporcional: cancele e crie outra. - **Estorno de PIX tem prazo** (regra do arranjo, não nossa). Passado o prazo do provedor, `POST /refund` dá erro e a devolução vira transferência manual. - **Estorno pode deixar o saldo negativo.** Debita o valor devolvido e a taxa da venda não volta. Saldo negativo bloqueia o saque até as próximas vendas quitarem. - **Listagens não filtram por período nem por status.** Paginam e ordenam, mas não dá pra pedir "as pagas de setembro". Filtre no seu lado ou guarde o que chega por webhook. - **Não há listagem global de estornos**, só por cobrança. - **Cliente não é objeto de 1ª classe.** Existe dentro da cobrança; não há `/v1/customers`. ## Boas práticas - Libere o produto no `charge.paid`, nunca na criação da cobrança. - Mande `Idempotency-Key` em toda criação. - Valide a assinatura do webhook com o corpo cru, sempre. - Guarde o `secret` do webhook na criação: não dá pra recuperar depois. - Use `metadata` pra amarrar a cobrança ao seu pedido; ela volta no webhook. - Em `charge.refunded`, cheque se foi total (`amount_refunded == amount`) antes de revogar acesso. - Conte com a taxa: cada venda estornada por inteiro custa a taxa daquela venda. - Pra varrer listagens, siga o `next_cursor` — nunca reconte do começo. - Trate `429` esperando o `Retry-After`, não repetindo em laço.