Pra agentes de IA
Esta documentação é otimizada para LLMs. Cada endpoint tem request/response COMPLETOS, sem omissões. Use os exemplos cURL como ponto de partida para geração de SDK em qualquer linguagem. O servidor responde JSON (UTF-8). Não há paginação por offset; use cursor nos endpoints de listagem.
Resumo rápido
| Base URL | https://bananafy.app/api/v1 |
| Auth | Authorization: Bearer bn_live_... |
| Content-Type | application/json |
| Encoding | UTF-8 |
| Rate limit | 60 req/min por chave (+10/min nos saques) |
| Idempotência | Header Idempotency-Key em POST /transactions e /withdrawals (TTL 24h) |
| Webhook | HMAC-SHA256 t=<ts>,v1=<sig> nos headers Bananafy-Signature e X-Bananafy-Signature |
| OpenAPI | /api/v1/openapi.json |
Autenticação
Toda chamada requer uma chave de API no header Authorization com esquema Bearer.
Gere a chave em Dashboard → API Keys. A chave começa com bn_live_ e aparece UMA VEZ — copie e guarde.
curl https://bananafy.app/api/v1/me \
-H"Authorization: Bearer bn_live_seu_token"Resposta 401 quando o token é inválido. Resposta 429 com header Retry-After quando excede o rate limit.
Formato de erro
Erros sempre vêm com este formato. Use o campo error.code pra branching no código (estável); o error.message é pra humanos.
{
"error": {
"code":"validation_error",
"message":"One or more fields are invalid",
"details": {"amount_cents": ["amount_cents min R$5,00 (500 cents)"] }
}
}Códigos comuns
| unauthorized | 401 · Token ausente, inválido ou expirado |
| forbidden | 403 · Token válido mas sem permissão pro recurso |
| validation_error | 422 · Campo com formato/valor inválido (veja details) |
| product_not_found | 404 · product_id não pertence à conta |
| product_inactive | 422 · Produto existe mas está desativado |
| rate_limited | 429 · Excedeu 60 req/min (veja header Retry-After) |
| acquirer_error | 502 · O adquirente (Stripe/SafePix/MP) falhou; veja details.attempts |
| internal_error | 500 · Bug servidor; reportar ao suporte |
Limites (rate limits)
Toda resposta da API pública inclui headers que informam sua cota atual. Convenção padrão da indústria (X-RateLimit-*), compatível com Stripe/GitHub — os SDKs oficiais expõem isso automaticamente.
Cotas por credencial
| Por API key autenticada | 60 requisições / minuto |
| Por IP com key válida | 45 requisições / minuto |
| Por IP sem Authorization | 30 requisições / minuto |
| Por IP com key inválida | 20 requisições / minuto |
Headers em toda resposta
HTTP/1.1 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1783493400
Content-Type: application/jsonX-RateLimit-Limit— teto da janela atual (60/min).X-RateLimit-Remaining— quantas chamadas restam antes do reset.X-RateLimit-Reset— epoch unix (segundos) de quando o contador zera.
Resposta 429 (estouro)
Quando você excede o limite, a resposta vem com Retry-After em segundos (RFC 6585) — respeite esse valor no seu retry. Não faça spam de retry sem backoff; contamos janela deslizante e você só prolonga o bloqueio.
HTTP/1.1 429 Too Many Requests
Retry-After: 34
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1783493434
Content-Type: application/json
{
"error": {
"code":"rate_limited",
"message":"Rate limit exceeded for this API key."
}
}POST /transactions — Criar cobrança
Endpoint principal. Cria uma cobrança (PIX ou cartão) sem precisar de produto/checkout pré-cadastrado. A maioria dos casos cabe aqui.
utm de cada cobrança. Capture utm_source, utm_medium, utm_campaign, utm_content, utm_term (+ fbclid/ttclid/gclid) da URL da SUA página de checkout e repasse no body. Sem isso a venda aparece na UTMify SEM origem e não é atribuída ao anúncio/campanha — o tracking não funciona. Basta configurar o token na aba Integrações + mandar os UTMs aqui; o envio à UTMify (waiting_payment + paid) é automático.product_id REAL (não dependa do ghost). Sem product_ida cobrança é acoplada a um produto interno (“ghost”). Isso causa 2 problemas na UTMify: (1) se você ativou filtro por produto na config da UTMify, a venda do ghost é excluída do envio(não aparece lá); (2) o nome do produto na UTMify fica como “API Charge” em vez do nome real. Então, pra UTMify funcionar 100%, crie um produto e envie o product_id.Onde pegar o ID: em /dashboard/produtos cada produto tem um badge com o ID — clique nele pra copiar. Ou liste via
GET /api/v1/products (campo id de cada item)./transactionsCria PIX ou cobrança de cartão. Retorna copy-paste do PIX ou URL hospedada do cartão.
Request body (JSON)
| Campo | Tipo | Descrição |
|---|---|---|
amount_cents | integer (req) | Valor em centavos. Mínimo 500 (R$ 5,00). |
method | "pix" | "card" | Default "pix". Ambos permitidos sem product_id. |
customer.email | string | Email do cliente. Default gerado automaticamente. |
customer.name | string | Nome do cliente. |
customer.cpf | string | CPF/CNPJ (com ou sem máscara). |
customer.phone | string | Telefone (E.164 ou formato BR). |
product_id | string ⚠️ | Opcional pra cobrar, mas OBRIGATÓRIO se você usa UTMify (senão a venda do ghost pode ser filtrada e mostra "API Charge"). Pegue em /dashboard/produtos (badge clica-copia) ou GET /api/v1/products. |
description | string | Texto livre que aparece no extrato/recibo. Max 300 chars. |
external_reference | string | Seu ID interno pra correlacionar. Max 120 chars. Volta em todos os webhooks + GET. |
metadata | object | Chave→valor livre (ex: {"attendant_code":"A123"}). Máx 20 chaves. Volta INTACTA nos webhooks e no GET /transactions. |
expires_in | integer | Segundos até expirar (PIX). 60-86400. Default 3600. |
split.recipient | string | Email OU user id de OUTRA conta Bananafy que recebe parte da venda. Pra split/revenue-share (ex: bot de Telegram repassando pro parceiro). |
split.amount_cents | integer | Valor FIXO do split em centavos. Use isto OU split.percent (não os dois). |
split.percent | number | Percentual (0.01–100) sobre o LÍQUIDO da venda (após taxas). Use isto OU split.amount_cents. |
utm.source | string ⚠️ | OBRIGATÓRIO p/ UTMify/atribuição. Capture da URL da sua página (ex: "fb"). |
utm.medium | string ⚠️ | OBRIGATÓRIO p/ UTMify. Capture da URL (ex: "video"). |
utm.campaign | string ⚠️ | OBRIGATÓRIO p/ UTMify. ID/nome da campanha, da URL. |
utm.content | string | Recomendado. ID do criativo/anúncio, da URL. |
utm.term | string | Recomendado. Placement/termo, da URL. |
utm.fbclid | string | Facebook click ID (da URL). Melhora a atribuição Meta. |
utm.gclid | string | Google click ID (da URL). |
utm.ttclid | string | TikTok click ID (da URL). |
Exemplo cURL — PIX simples
curl -X POST https://bananafy.app/api/v1/transactions \
-H"Authorization: Bearer bn_live_seu_token" \
-H"Content-Type: application/json" \
-d '{
"amount_cents": 1990,
"method":"pix",
"product_id":"prd_SEU_ID_AQUI",
"customer": {
"email":"joao@example.com",
"name":"João Silva",
"cpf":"11144477735"
},
"description":"Mensalidade Plano Pro",
"external_reference":"order_42",
"utm": {
"source":"fb",
"medium":"video",
"campaign":"camp-black-friday",
"content":"criativo-A",
"term":"feed",
"fbclid":"IwAR123...",
"ttclid":"",
"gclid":""
}
}'⚠️ product_id: troque prd_SEU_ID_AQUI pelo ID do seu produto (pegue em /dashboard/produtos — badge clica-copia — ou via GET /api/v1/products). Use-o quando usar UTMify. utm: pegue os valores dos parâmetros da URL da sua página (ex: ?utm_source=fb&utm_campaign=...) — sem eles a UTMify não atribui a venda.
Resposta sucesso (PIX) · HTTP 200
{
"id":"tx_clxabc123",
"object":"transaction",
"status":"PENDING",
"method":"PIX",
"amount_cents": 1990,
"fee_cents": 449,
"net_cents": 1541,
"currency":"BRL",
"pix": {
"copy_paste":"00020126580014BR.GOV.BCB.PIX...",
"expires_at":"2026-06-01T16:30:00.000Z"
},
"split": null,
"created_at":"2026-06-01T15:30:00.000Z"
}Exemplo cURL — PIX com split (revenue share / bot Telegram)
curl -X POST https://bananafy.app/api/v1/transactions \
-H"Authorization: Bearer bn_live_seu_token" \
-H"Content-Type: application/json" \
-d '{
"amount_cents": 5000,
"method":"pix",
"customer": {"email":"cliente@example.com" },
"description":"Acesso VIP",
"split": {
"recipient":"parceiro@example.com",
"percent": 30
}
}'Quando esse PIX for pago, 30% do líquido cai automaticamente no saldo da conta parceiro@example.com e o restante no seu. Use amount_cents em vez de percent pra um valor fixo. O recebedor precisa ser uma conta Bananafy (identifique por email ou user id). Funciona igual em POST /subscriptions (split aplicado em toda renovação).
Exemplo cURL — Cartão
curl -X POST https://bananafy.app/api/v1/transactions \
-H"Authorization: Bearer bn_live_seu_token" \
-H"Content-Type: application/json" \
-d '{
"amount_cents": 9990,
"method":"card",
"customer": {
"email":"maria@example.com",
"name":"Maria Souza",
"cpf":"52998224725"
},
"description":"Curso Premium"
}'Resposta sucesso (CARD) · HTTP 200
{
"id":"tx_clxdef456",
"object":"transaction",
"status":"PENDING",
"method":"CARD",
"amount_cents": 9990,
"fee_cents": 1349,
"net_cents": 8641,
"currency":"BRL",
"card": {
"hosted_url":"https://bananafy.app/api/v1/transactions/tx_clxdef456/card-redirect",
"message":"Redirect customer to hosted_url to enter card details (Stripe Elements, PCI-compliant)."
},
"created_at":"2026-06-01T15:30:00.000Z"
}GET /transactions — Listar
/transactionsLista transações. Ordenação created_at DESC (desempate por id) — cursor 100% estável.
Query params
| Campo | Tipo | Descrição |
|---|---|---|
limit | integer | Máx 100. Default 20. |
cursor | string | ID da última transação da página anterior (next_cursor). |
status | PENDING|PAID|EXPIRED|REFUNDED|CHARGEBACK | Filtra por status. |
method | PIX|CARD|APPLE_PAY|GOOGLE_PAY | Filtra por método. |
external_reference | string | Filtra pelo seu external_reference exato. |
created_at_from | ISO 8601 | Só txs criadas A PARTIR desta data (gte). |
created_at_to | ISO 8601 | Só txs criadas ATÉ esta data (lte). |
Cada item traz external_reference, metadata, fee_cents, net_cents, status e tracking (UTMs). Reconciliação por janela: ?created_at_from=2026-06-01T00:00:00Z&created_at_to=2026-06-02T00:00:00Z.
curl https://bananafy.app/api/v1/transactions?status=PAID&limit=50 \
-H"Authorization: Bearer bn_live_seu_token"{
"data": [
{
"id":"tx_clxabc123",
"object":"transaction",
"product_id":"prd_xxx",
"checkout_id":"ck_xxx",
"customer": {"email":"...","name":"...","cpf":"...","phone":"..." },
"amount_cents": 1990,
"fee_cents": 449,
"net_cents": 1541,
"currency":"BRL",
"method":"PIX",
"status":"PAID",
"provider":"safepix",
"provider_id":"tx_safepix_xxx",
"pix": {"qr_code":"...","copy_paste":"...","expires_at":"..." },
"card": null,
"tracking": {"utm_source":"...","utm_medium":"...","fbclid":"..." },
"split": {"recipient":"parceiro@example.com","amount_cents": 450 },
"paid_at":"2026-06-01T15:32:00.000Z",
"created_at":"2026-06-01T15:30:00.000Z",
"updated_at":"2026-06-01T15:32:00.000Z"
}
],
"has_more": true,
"next_cursor":"tx_clxabc123"
}GET /me — Conta atual
/meRetorna info da conta dona da API key.
curl https://bananafy.app/api/v1/me -H"Authorization: Bearer bn_live_seu_token"{
"id":"usr_xxx",
"email":"voce@example.com",
"name":"Você",
"balance_cents": 12345,
"api_card_enabled": false
}GET /balance — Saldo
/balanceSaldo detalhado da conta: disponível (sacável), pendente em holding, retenção de cartão e saques em aberto.
curl https://bananafy.app/api/v1/balance -H"Authorization: Bearer bn_live_seu_token"Resposta · HTTP 200
{
"object":"balance",
"currency":"BRL",
"available_cents": 50000, // SACÁVEL agora (já liberado)
"pending_pix_cents": 12000, // PIX pago, ainda em holding
"pending_card_cents": 0, // Cartão pago, ainda em holding (D+X)
"retained_card_cents": 0, // Retenção de cartão (libera no fim do período)
"in_withdrawal_cents": 0, // Saques abertos já debitados
"upcoming_releases": [
{"transaction_id":"tx_xxx","date":"2026-06-02T10:00:00.000Z","cents": 12000,"kind":"pix_release","method":"PIX" }
]
}O valor que você pode sacar via API é sempre available_cents. Os demais campos são informativos (calendário de liberação).
Saques (Withdrawals)
/withdrawalsSolicita um saque PIX ou USDT. Débito atômico: NUNCA saca mais que o saldo disponível.
/withdrawalsLista os saques da conta (cursor).
/withdrawals/{id}Status de um saque específico.
Segurança. O débito do saldo é feito de forma atômica e condicional no banco (WHERE saldo >= valor) — é impossível sacar mais do que você tem. Só pode haver 1 saque em aberto por conta de cada vez, e toda solicitação passa por uma auditoria automática antes de ser aceita. A taxa de saque sai do valor solicitado (você recebe amount_cents − fee_cents).
POST /withdrawals · body
| Campo | Tipo | Descrição |
|---|---|---|
amount_cents | integer (req) | Valor BRUTO do saque em centavos. PIX mín R$ 30,00 (3000). USDT mín R$ 2.000,00 (200000). |
method | "pix" | "usdt_solana" | Default "pix". |
pix_key | string | Obrigatório p/ PIX. Sua chave Pix. |
pix_key_type | cpf|cnpj|evp|phone|email | Obrigatório p/ PIX. Tipo da chave. |
wallet_address | string | Obrigatório p/ USDT. Endereço da carteira Solana. |
Exemplo cURL — saque PIX
curl -X POST https://bananafy.app/api/v1/withdrawals \
-H"Authorization: Bearer bn_live_seu_token" \
-H"Content-Type: application/json" \
-d '{
"amount_cents": 5000,
"method":"pix",
"pix_key":"voce@example.com",
"pix_key_type":"email"
}'Resposta · HTTP 201
{
"id":"wd_clxabc123",
"object":"withdrawal",
"status":"PENDING",
"method":"pix",
"amount_cents": 5000,
"fee_cents": 990,
"net_cents": 4010,
"pix_key":"voce@example.com",
"pix_key_type":"email",
"wallet_address": null,
"fail_reason": null,
"provider_id": null,
"paid_at": null,
"created_at":"2026-06-01T15:30:00.000Z",
"updated_at":"2026-06-01T15:30:00.000Z"
}Erros possíveis
| Campo | Tipo | Descrição |
|---|---|---|
insufficient_balance | 422 | Saldo disponível menor que o valor pedido. |
amount_below_minimum | 422 | Abaixo do mínimo (PIX R$ 30 / USDT R$ 2.000). |
withdrawal_in_progress | 409 | Já existe um saque em aberto. Aguarde finalizar. |
audit_blocked | 422 | Auditoria automática bloqueou (drift de saldo, refund rate, etc). |
blocked | 403 | Conta bloqueada para saques. |
insufficient_scope | 403 | A API key não tem o escopo "withdrawals". |
GET /withdrawals — lista
curl"https://bananafy.app/api/v1/withdrawals?status=PAID&limit=50" \
-H"Authorization: Bearer bn_live_seu_token"Status possíveis: PENDING, PROCESSING, PAID, FAILED, REJECTED.
Produtos
/productsLista produtos da conta.
/productsCria produto. Preço mínimo R$ 5,00 (500 cents).
/products/{id}Detalha produto por ID.
/products/{id}Atualiza campos do produto.
POST /products · body
| Campo | Tipo | Descrição |
|---|---|---|
name | string (req) | Nome do produto. Max 200. |
price_cents | integer (req) | Preço em centavos. Min 500 = R$ 5,00. |
description | string | Descrição. Max 2000. |
image | string (url) | URL HTTPS de imagem (jpg/png/webp). |
type | "DIGITAL" | "PHYSICAL" | "SUBSCRIPTION" | Default DIGITAL. |
interval_days | integer | Só se type=SUBSCRIPTION. Default 30. |
active | boolean | Default true. |
curl -X POST https://bananafy.app/api/v1/products \
-H"Authorization: Bearer bn_live_seu_token" \
-H"Content-Type: application/json" \
-d '{
"name":"Plano Pro Mensal",
"price_cents": 1990,
"description":"Acesso completo por 30 dias",
"type":"SUBSCRIPTION",
"interval_days": 30
}'Checkouts
Um Checkout é uma página de venda pública hospedada em bananafy.app/c/{slug}. Cada produto pode ter vários checkouts (ofertas diferentes).
/checkoutsLista checkouts da conta.
/checkoutsCria checkout. Requer product_id existente.
/checkouts/{id}Detalha checkout.
/checkouts/{id}Atualiza checkout.
POST /checkouts · body
| Campo | Tipo | Descrição |
|---|---|---|
product_id | string (req) | ID do produto. |
slug | string | Slug da URL. Auto-gerado se omitido. |
payment_methods | array | ["pix", "card", "apple_pay", "google_pay"] |
price_cents_override | integer | Sobrescreve o preço do produto pra esse checkout. |
thank_you_url | string (url) | Pra onde redirecionar após pagar. |
back_redirect_url | string (url) | Back-redirect quando user sai sem pagar. |
video_url | string (url) | VSL no topo do checkout. |
facebook_pixel_id | string | Override do pixel global. |
tiktok_pixel_id | string | Override do pixel global. |
SDKs oficiais
Integre em minutos usando um SDK oficial em vez de fetchmanual. Cada SDK cobre transações, assinaturas, saldo, saques e verificação de webhook com types nativos.
Node.js / TypeScript
npm install @bananafy/nodeimport { Bananafy, verifyWebhook } from"@bananafy/node";
const bananafy = new Bananafy({ apiKey: process.env.BANANAFY_KEY! });
// Criar cobrança PIX
const tx = await bananafy.transactions.createPix({
amount_cents: 4990,
customer: {
email:"cliente@exemplo.com",
name:"Maria",
cpf:"12345678900",
phone:"11999999999",
},
product_name:"Assinatura Pro",
});
console.log(tx.pix?.copy_paste); // BR Code EMV
// Consultar saldo
const balance = await bananafy.balance.get();
console.log("Disponível:", balance.available_cents / 100,"BRL");
// Criar assinatura mensal
const sub = await bananafy.subscriptions.create({
amount_cents: 4990,
interval:"monthly",
customer: { email:"cliente@exemplo.com", name:"Maria", cpf:"12345678900" },
});
// Verificar webhook (Express)
app.post("/webhook", express.raw({ type:"application/json" }), (req, res) => {
const ok = verifyWebhook(
req.body,
req.headers["x-bananafy-signature"] as string,
process.env.WEBHOOK_SECRET!,
);
if (!ok) return res.status(401).send("Invalid signature");
const event = JSON.parse(req.body.toString());
console.log(event.type, event.data);
res.sendStatus(200);
});Python
pip install bananafyimport os
from bananafy import Bananafy, verify_webhook
bn = Bananafy(api_key=os.environ["BANANAFY_KEY"])
# Criar cobrança PIX
tx = bn.transactions.create_pix(
amount_cents=4990,
customer={"email":"cliente@exemplo.com","name":"Maria","cpf":"12345678900","phone":"11999999999"},
product_name="Assinatura Pro",
)
print(tx["pix"]["copy_paste"])
# Assinatura mensal
sub = bn.subscriptions.create(
amount_cents=4990,
interval="monthly",
customer={"email":"cliente@exemplo.com","name":"Maria","cpf":"12345678900"},
)PHP
composer require bananafy/bananafy<?php
use Bananafy\Bananafy;
use function Bananafy\verifyWebhook;
$bn = new Bananafy(apiKey: getenv('BANANAFY_KEY'));
// Criar cobrança PIX
$tx = $bn->transactions->createPix([
'amount_cents' => 4990,
'customer' => [
'email' => 'cliente@exemplo.com',
'name' => 'Maria',
'cpf' => '12345678900',
'phone' => '11999999999',
],
'product_name' => 'Assinatura Pro',
]);
echo $tx['pix']['copy_paste'];
// Assinatura mensal
$sub = $bn->subscriptions->create([
'amount_cents' => 4990,
'interval' => 'monthly',
'customer' => ['email' => 'cliente@exemplo.com', 'name' => 'Maria', 'cpf' => '12345678900'],
]);Go
go get github.com/luhandrade11/bananafy-gopackage main
import (
"context"
"fmt"
"os"
"github.com/luhandrade11/bananafy-go"
)
func main() {
bn := bananafy.New(os.Getenv("BANANAFY_KEY"))
ctx := context.Background()
tx, err := bn.CreatePix(ctx, bananafy.CreatePixInput{
AmountCents: 4990,
Customer: bananafy.Customer{
Email:"cliente@exemplo.com",
Name:"Maria",
CPF:"12345678900",
Phone:"11999999999",
},
ProductName:"Assinatura Pro",
})
if err != nil {
panic(err)
}
fmt.Println("PIX copy-paste:", tx.PIX.CopyPaste)
}Ruby
gem install bananafyrequire"bananafy"
bn = Bananafy::Client.new(api_key: ENV.fetch("BANANAFY_KEY"))
# Criar cobrança PIX
tx = bn.transactions.create_pix(
amount_cents: 4990,
customer: {
email:"cliente@exemplo.com",
name:"Maria",
cpf:"12345678900",
phone:"11999999999",
},
product_name:"Assinatura Pro",
)
puts tx["pix"]["copy_paste"]
# Assinatura mensal
sub = bn.subscriptions.create(
amount_cents: 4990,
interval:"monthly",
customer: { email:"cliente@exemplo.com", name:"Maria", cpf:"12345678900" },
)Todos os SDKs
Node.js, Python, PHP, Go e Ruby cobrem a mesma superfície da API + verificação HMAC de webhook. Escolha a linguagem que combina com sua stack — o payload/response é sempre o mesmo.
Webhooks (eventos)
Eventos são enviados via POST pra URL configurada em Dashboard → Integrações. Cada webhook tem um secret próprio (whsec_...), mostrado quando você cadastra a URL. É com ele que você valida a assinatura HMAC.
Eventos disponíveis
| transaction.created | Tx criada (PIX gerado ou cartão iniciado) |
| transaction.paid | Pagamento confirmado |
| transaction.refunded | Reembolsada |
| transaction.chargeback | Chargeback (cartão) |
| transaction.expired | PIX expirou |
| transaction.failed | Falha no dispatcher do PSP |
| transaction.receipt_uploaded | Lead subiu comprovante |
| withdrawal.paid | Saque pago |
| withdrawal.failed | Saque recusado/falhou (saldo estornado) |
Envelope do evento
{
"id":"evt_<deliveryId>", // ESTÁVEL entre retentativas — dedupe por ele
"type":"transaction.paid",
"created": 1730476320, // unix timestamp (segundos)
"data": { /* objeto do evento (ver abaixo) */ }
}data — transaction.* (created / paid / refunded / chargeback / expired)
{
"id":"tx_clxabc123",
"product_id":"prd_xxx",
"checkout_id": null,
"amount_cents": 1990,
"fee_cents": 449,
"net_cents": 1541,
"currency":"BRL",
"method":"PIX",
"status":"PAID",
"provider":"safepix",
"provider_id":"...",
"external_reference":"order_42",
"metadata": {"attendant_code":"A123" },
"customer": {"email":"...","name":"...","cpf":"...","phone":"..." },
"tracking": {"utm_source":"fb","utm_medium":"video","utm_campaign":"...",
"utm_content":"...","utm_term":"...","fbclid":"...",
"ttclid":"...","gclid":"..." },
"receipt_url": null,
"paid_at":"2026-06-01T15:32:00.000Z",
"created_at":"2026-06-01T15:30:00.000Z"
}external_reference, metadata e tracking (UTMs) voltam em TODOS os eventos de transação — inclusive refunded e chargeback.
data — withdrawal.paid / withdrawal.failed
{
"id":"wd_xxx",
"object":"withdrawal",
"status":"PAID",
"method":"pix",
"amount_cents": 5000,
"fee_cents": 990,
"net_cents": 4010,
"pix_key":"voce@example.com",
"wallet_address": null,
"fail_reason": null,
"paid_at":"2026-06-01T16:00:00.000Z"
}Assinatura (HMAC-SHA256) — header e formato exato
A assinatura vem em dois headers com o MESMO valor: Bananafy-Signature (canônico) e X-Bananafy-Signature (alias). Formato:
Bananafy-Signature: t=1730476320,v1=9f86d081884c7d659...t= unix timestamp usado na assinatura.v1= HMAC-SHA256 (hex) do payload assinado${t}.${rawBody}— ou seja, o timestamp + ponto + o corpo CRU do request (a string JSON exata, não re-serializada).- Anti-replay: rejeite se
testiver fora de uma janela (ex: ±5 min) do horário atual. - Algoritmo:
HMAC-SHA256. Secret: owhsec_do webhook.
Verificação de assinatura (Node.js) — correta
import crypto from"node:crypto";
// rawBody = corpo CRU (string), NÃO o objeto re-serializado.
function verifyWebhook(rawBody, headerValue, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
headerValue.split(",").map((kv) => kv.split("="))
); // { t, v1 }
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // replay
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(parts.v1)
);
}Verificação de assinatura (Python)
import hmac, hashlib, time
def verify_webhook(raw_body: bytes, header_value: str, secret: str, tol_sec=300):
parts = dict(kv.split("=") for kv in header_value.split(","))
t = int(parts.get("t", 0))
if not t or abs(time.time() - t) > tol_sec:
return False # replay attack
expected = hmac.new(
secret.encode(),
f"{t}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, parts.get("v1",""))Verificação de assinatura (PHP)
<?php
function verifyWebhook(string $rawBody, string $header, string $secret, int $tolSec = 300): bool {
parse_str(str_replace(',', '&', $header), $parts);
$t = (int)($parts['t'] ?? 0);
if (!$t || abs(time() - $t) > $tolSec) return false;
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1'] ?? '');
}Retentativas (retry)
| Sucesso | Qualquer 2xx (usamos res.ok). Responda 200 rápido (<5s). |
| Tentativas | Até 8 no total. |
| Backoff | 30s, 2min, 10min, 30min, 1h, 3h, 6h, 24h |
| evt id | MESMO id em todas as retentativas (dedupe seguro). Só muda t/assinatura. |
| IPs de origem | Sem IPs fixos (infra serverless). Valide por HMAC, não por IP. |
evt id é estável entre retentativas, use-o pra deduplicar (idempotência no recebedor).Taxas (referência)
As taxas variam por conta. Consulte GET /me pra ver os números atuais da sua conta, ou Dashboard → Perfil.
| PIX (padrão) | 6,99% + R$ 2,00 (varia por conta — veja GET /me) |
| Cartão (white) | 9,90% + R$ 3,60 · retenção 10% × 90 dias |
| Cartão (black) | 35% + R$ 10,00 · retenção 20% × 90 dias |
| Liberação (hold) | PIX: imediato · Cartão: D+30 (depois a retenção libera em D+90) |
| Saque PIX | R$ 9,90 (fixo) · mínimo R$ 30,00 |
| Saque USDT/Solana | 6,5% + R$ 10,00 · mínimo R$ 2.000,00 |
Quickstart (Node.js)
// 1. Cria PIX (sem produto, funciona pra qualquer conta)
const res = await fetch("https://bananafy.app/api/v1/transactions", {
method:"POST",
headers: {
"Authorization": `Bearer ${process.env.BANANAFY_API_KEY}`,
"Content-Type":"application/json",
},
body: JSON.stringify({
amount_cents: 1990,
method:"pix",
customer: {
email:"cliente@example.com",
name:"Cliente Exemplo",
cpf:"11144477735",
},
description:"Pedido #42",
external_reference:"order_42",
}),
});
const tx = await res.json();
if (!res.ok) throw new Error(tx.error.message);
// 2. Mostra QR pro cliente
console.log("Copia-cola PIX:", tx.pix.copy_paste);
console.log("Expira em:", tx.pix.expires_at);
// 3. Espera webhook 'transaction.paid' chegar no seu endpoint
// OU pollie GET /transactions/{id} periodicamente
// OU melhor: configure o webhook em Dashboard → IntegraçõesBrand tokens (paleta machine-readable)
Precisa customizar seu checkout mas quer manter cor de fundo, botão e fonte alinhados com a Bananafy? Puxe os tokens direto do endpoint — zero hardcode, atualização automática quando a gente subir uma versão nova da paleta.
/brand/tokens.jsonPúblico (sem auth). Cache CDN 24h. Retorna paleta + tipografia + URLs dos SVG assets.
Resposta (fragmento)
{
"version":"1.0.0",
"scheme":"bananafy-brand-tokens/v1",
"colors": {
"banana": {
"400": {"hex":"#FFD84D","note":"Amarelo primário do logo" },
"500": {"hex":"#F5C518" }
},
"peel": {
"700": {"hex":"#2a1810","note":"Ink — footer e texto principal" }
},
"accent": {
"lime-400": {"hex":"#a3e635","note":"Dashboard CTA (Tailwind default)" }
},
"aliases": {
"ink": {"hex":"#2a1810","alias_of":"peel-700" },
"cream": {"hex":"#FFFFFF","alias_of":"banana-50" }
}
},
"typography": {
"sans": {"family":"Geist Sans","weights": [400, 500, 600, 700, 800] },
"mono": {"family":"Geist Mono","weights": [400, 500, 600, 700] }
},
"assets": {
"logo_symbol_svg":"https://bananafy.app/logo-bananafy.png",
"powered_by_dark_svg":"https://bananafy.app/brand/powered-by.svg",
"powered_by_light_svg":"https://bananafy.app/brand/powered-by-light.svg"
}
}Uso no seu checkout React
import { useEffect, useState } from"react";
type BrandTokens = {
colors: { banana: Record<string, { hex: string }>; peel: Record<string, { hex: string }> };
typography: { sans: { family: string; fallback: string } };
assets: { powered_by_dark_svg: string };
};
export function BananafyPoweredCheckout() {
const [t, setT] = useState<BrandTokens | null>(null);
useEffect(() => {
fetch("https://bananafy.app/api/v1/brand/tokens.json")
.then(r => r.json())
.then(setT);
}, []);
if (!t) return null;
return (
<div style={{
background: t.colors.banana["50"].hex,
color: t.colors.peel["700"].hex,
fontFamily: `${t.typography.sans.family}, ${t.typography.sans.fallback}`,
}}>
{/* seu checkout aqui */}
<img src={t.assets.powered_by_dark_svg} alt="Powered by Bananafy" height={32} />
</div>
);
}public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800. O CDN da Vercel guarda 24h; o browser dá refresh a cada 1h. Se a gente atualizar a paleta, bumpamos o campo version pra você invalidar cache client-side. CORS liberado (*) — pode chamar de qualquer domínio no cliente.