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 URLhttps://bananafy.app/api/v1
AuthAuthorization: Bearer bn_live_...
Content-Typeapplication/json
EncodingUTF-8
Rate limit60 req/min por chave (+10/min nos saques)
IdempotênciaHeader Idempotency-Key em POST /transactions e /withdrawals (TTL 24h)
WebhookHMAC-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.

bash
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.

json
{
"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

unauthorized401 · Token ausente, inválido ou expirado
forbidden403 · Token válido mas sem permissão pro recurso
validation_error422 · Campo com formato/valor inválido (veja details)
product_not_found404 · product_id não pertence à conta
product_inactive422 · Produto existe mas está desativado
rate_limited429 · Excedeu 60 req/min (veja header Retry-After)
acquirer_error502 · O adquirente (Stripe/SafePix/MP) falhou; veja details.attempts
internal_error500 · 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 autenticada60 requisições / minuto
Por IP com key válida45 requisições / minuto
Por IP sem Authorization30 requisições / minuto
Por IP com key inválida20 requisições / minuto

Headers em toda resposta

http
HTTP/1.1 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1783493400
Content-Type: application/json
  • X-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
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."
 }
}
Precisa de mais? Contas em plano Alto Volume podem pedir aumento no suporte informando volume médio de req/s e padrão de burst. Padrão pra checkout customizado (poucas chamadas por venda) resolve 99% dos casos.

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.

Sem product_id: tanto PIX quanto cartão funcionam pra TODA conta. O servidor cria um ghost product automaticamente pra acoplar a tx.
⚠️ OBRIGATÓRIO pra rastreamento (UTMify / Pixel / atribuição): diferente do checkout hospedado — que captura os UTMs da URL sozinho — na API VOCÊ precisa enviar os UTMs no campo 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.
🪤 Usa UTMify? Passe um 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).
POST/transactions

Cria PIX ou cobrança de cartão. Retorna copy-paste do PIX ou URL hospedada do cartão.

Request body (JSON)

CampoTipoDescrição
amount_centsinteger (req)Valor em centavos. Mínimo 500 (R$ 5,00).
method"pix" | "card"Default "pix". Ambos permitidos sem product_id.
customer.emailstringEmail do cliente. Default gerado automaticamente.
customer.namestringNome do cliente.
customer.cpfstringCPF/CNPJ (com ou sem máscara).
customer.phonestringTelefone (E.164 ou formato BR).
product_idstring ⚠️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.
descriptionstringTexto livre que aparece no extrato/recibo. Max 300 chars.
external_referencestringSeu ID interno pra correlacionar. Max 120 chars. Volta em todos os webhooks + GET.
metadataobjectChave→valor livre (ex: {"attendant_code":"A123"}). Máx 20 chaves. Volta INTACTA nos webhooks e no GET /transactions.
expires_inintegerSegundos até expirar (PIX). 60-86400. Default 3600.
split.recipientstringEmail 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_centsintegerValor FIXO do split em centavos. Use isto OU split.percent (não os dois).
split.percentnumberPercentual (0.01–100) sobre o LÍQUIDO da venda (após taxas). Use isto OU split.amount_cents.
utm.sourcestring ⚠️OBRIGATÓRIO p/ UTMify/atribuição. Capture da URL da sua página (ex: "fb").
utm.mediumstring ⚠️OBRIGATÓRIO p/ UTMify. Capture da URL (ex: "video").
utm.campaignstring ⚠️OBRIGATÓRIO p/ UTMify. ID/nome da campanha, da URL.
utm.contentstringRecomendado. ID do criativo/anúncio, da URL.
utm.termstringRecomendado. Placement/termo, da URL.
utm.fbclidstringFacebook click ID (da URL). Melhora a atribuição Meta.
utm.gclidstringGoogle click ID (da URL).
utm.ttclidstringTikTok click ID (da URL).

Exemplo cURL — PIX simples

bash
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

json
{
"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)

bash
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

bash
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

json
{
"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

GET/transactions

Lista transações. Ordenação created_at DESC (desempate por id) — cursor 100% estável.

Query params

CampoTipoDescrição
limitintegerMáx 100. Default 20.
cursorstringID da última transação da página anterior (next_cursor).
statusPENDING|PAID|EXPIRED|REFUNDED|CHARGEBACKFiltra por status.
methodPIX|CARD|APPLE_PAY|GOOGLE_PAYFiltra por método.
external_referencestringFiltra pelo seu external_reference exato.
created_at_fromISO 8601Só txs criadas A PARTIR desta data (gte).
created_at_toISO 8601Só 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.

bash
curl https://bananafy.app/api/v1/transactions?status=PAID&limit=50 \
 -H"Authorization: Bearer bn_live_seu_token"
json
{
"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

GET/me

Retorna info da conta dona da API key.

bash
curl https://bananafy.app/api/v1/me -H"Authorization: Bearer bn_live_seu_token"
json
{
"id":"usr_xxx",
"email":"voce@example.com",
"name":"Você",
"balance_cents": 12345,
"api_card_enabled": false
}

GET /balance — Saldo

GET/balance

Saldo detalhado da conta: disponível (sacável), pendente em holding, retenção de cartão e saques em aberto.

bash
curl https://bananafy.app/api/v1/balance -H"Authorization: Bearer bn_live_seu_token"

Resposta · HTTP 200

json
{
"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)

POST/withdrawals

Solicita um saque PIX ou USDT. Débito atômico: NUNCA saca mais que o saldo disponível.

GET/withdrawals

Lista os saques da conta (cursor).

GET/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

CampoTipoDescrição
amount_centsinteger (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_keystringObrigatório p/ PIX. Sua chave Pix.
pix_key_typecpf|cnpj|evp|phone|emailObrigatório p/ PIX. Tipo da chave.
wallet_addressstringObrigatório p/ USDT. Endereço da carteira Solana.

Exemplo cURL — saque PIX

bash
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

json
{
"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

CampoTipoDescrição
insufficient_balance422Saldo disponível menor que o valor pedido.
amount_below_minimum422Abaixo do mínimo (PIX R$ 30 / USDT R$ 2.000).
withdrawal_in_progress409Já existe um saque em aberto. Aguarde finalizar.
audit_blocked422Auditoria automática bloqueou (drift de saldo, refund rate, etc).
blocked403Conta bloqueada para saques.
insufficient_scope403A API key não tem o escopo "withdrawals".

GET /withdrawals — lista

bash
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

GET/products

Lista produtos da conta.

POST/products

Cria produto. Preço mínimo R$ 5,00 (500 cents).

GET/products/{id}

Detalha produto por ID.

PATCH/products/{id}

Atualiza campos do produto.

POST /products · body

CampoTipoDescrição
namestring (req)Nome do produto. Max 200.
price_centsinteger (req)Preço em centavos. Min 500 = R$ 5,00.
descriptionstringDescrição. Max 2000.
imagestring (url)URL HTTPS de imagem (jpg/png/webp).
type"DIGITAL" | "PHYSICAL" | "SUBSCRIPTION"Default DIGITAL.
interval_daysintegerSó se type=SUBSCRIPTION. Default 30.
activebooleanDefault true.
bash
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).

GET/checkouts

Lista checkouts da conta.

POST/checkouts

Cria checkout. Requer product_id existente.

GET/checkouts/{id}

Detalha checkout.

PATCH/checkouts/{id}

Atualiza checkout.

POST /checkouts · body

CampoTipoDescrição
product_idstring (req)ID do produto.
slugstringSlug da URL. Auto-gerado se omitido.
payment_methodsarray["pix", "card", "apple_pay", "google_pay"]
price_cents_overrideintegerSobrescreve o preço do produto pra esse checkout.
thank_you_urlstring (url)Pra onde redirecionar após pagar.
back_redirect_urlstring (url)Back-redirect quando user sai sem pagar.
video_urlstring (url)VSL no topo do checkout.
facebook_pixel_idstringOverride do pixel global.
tiktok_pixel_idstringOverride 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

bash
npm install @bananafy/node
typescript
import { 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

bash
pip install bananafy
python
import 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

bash
composer require bananafy/bananafy
php
<?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

bash
go get github.com/luhandrade11/bananafy-go
go
package 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

bash
gem install bananafy
ruby
require"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.createdTx criada (PIX gerado ou cartão iniciado)
transaction.paidPagamento confirmado
transaction.refundedReembolsada
transaction.chargebackChargeback (cartão)
transaction.expiredPIX expirou
transaction.failedFalha no dispatcher do PSP
transaction.receipt_uploadedLead subiu comprovante
withdrawal.paidSaque pago
withdrawal.failedSaque recusado/falhou (saldo estornado)

Envelope do evento

json
{
"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)

json
{
"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

json
{
"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:

text
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 t estiver fora de uma janela (ex: ±5 min) do horário atual.
  • Algoritmo: HMAC-SHA256. Secret: o whsec_ do webhook.

Verificação de assinatura (Node.js) — correta

javascript
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)

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
<?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)

SucessoQualquer 2xx (usamos res.ok). Responda 200 rápido (<5s).
TentativasAté 8 no total.
Backoff30s, 2min, 10min, 30min, 1h, 3h, 6h, 24h
evt idMESMO id em todas as retentativas (dedupe seguro). Só muda t/assinatura.
IPs de origemSem IPs fixos (infra serverless). Valide por HMAC, não por IP.
Responda 200 OKrápido (<5s) pra não causar retry. Processe em fila se a lógica for pesada. Como o 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 PIXR$ 9,90 (fixo) · mínimo R$ 30,00
Saque USDT/Solana6,5% + R$ 10,00 · mínimo R$ 2.000,00

Quickstart (Node.js)

javascript
// 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ções

Brand 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.

GET/brand/tokens.json

Público (sem auth). Cache CDN 24h. Retorna paleta + tipografia + URLs dos SVG assets.

Resposta (fragmento)

json
{
"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

tsx
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>
 );
}
Cache-Control: 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.