RPG CONNECTA · RECEITAS

Problema comum, código pronto.

Copie e rode. Todas funcionam agora com a chave demo pública, sem cadastro. Para dados reais, troque RPG_API_KEY e RPG_CONNECTION_ID.

Versão para agentes e LLMs: /developers/recipes.md · Contrato completo: OpenAPI · Sua chave em 1 clique: POST /api/connecta/v1/auth/device

Fluxo de caixa por mês

Problema: Quanto entrou, saiu e sobrou em cada mês?

Como: Uma chamada ao /summary agrupado por mês. Pendentes ficam de fora e são contados em excluded.

curl
curl -s -H "Authorization: Bearer rpg_test_0000000000000000_demo_public_read_only_sandbox_key_xxxxx" "https://www.rpgcapital.com.br/api/connecta/v1/connections/00000000-0000-4000-8000-0000000000c1/summary?group_by=month"
Node.js 18+ (sem dependências)
// Roda como está (Node 18+, sem dependências) com a chave demo pública.
// Para dados reais: RPG_API_KEY=rpg_live_... RPG_CONNECTION_ID=<uuid> node arquivo.mjs
const BASE = process.env.RPG_API_BASE || 'https://www.rpgcapital.com.br/api/connecta/v1'
const KEY = process.env.RPG_API_KEY || 'rpg_test_0000000000000000_demo_public_read_only_sandbox_key_xxxxx'
const CONN = process.env.RPG_CONNECTION_ID || '00000000-0000-4000-8000-0000000000c1'
async function api(path) {
  const r = await fetch(BASE + path, { headers: { Authorization: 'Bearer ' + KEY } })
  const body = await r.json()
  if (!r.ok) throw new Error(body.error?.code + ': ' + body.error?.message + ' (request ' + body.request_id + ')')
  return body
}
const brl = (minor) => (minor / 100).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })

const { data } = await api('/connections/' + CONN + '/summary?group_by=month')
for (const m of data.groups) console.log(m.label, 'entrou', brl(m.inflow_minor), 'saiu', brl(m.outflow_minor), 'sobrou', brl(m.net_minor))
console.log('Saldo hoje (contas de depósito):', brl(data.balances_by_currency.BRL?.balance_minor ?? 0))
console.log('Fora da conta (pendentes):', data.excluded.pending)
Python 3.9+ (sem dependências)
# Roda como está (Python 3.9+, sem dependências) com a chave demo pública.
# Para dados reais: RPG_API_KEY=rpg_live_... RPG_CONNECTION_ID=<uuid> python arquivo.py
import json, os, urllib.request, urllib.error
BASE = os.environ.get("RPG_API_BASE", "https://www.rpgcapital.com.br/api/connecta/v1")
KEY = os.environ.get("RPG_API_KEY", "rpg_test_0000000000000000_demo_public_read_only_sandbox_key_xxxxx")
CONN = os.environ.get("RPG_CONNECTION_ID", "00000000-0000-4000-8000-0000000000c1")

def api(path):
    req = urllib.request.Request(BASE + path, headers={"Authorization": "Bearer " + KEY})
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return json.load(r)
    except urllib.error.HTTPError as e:
        body = json.load(e)
        raise SystemExit(f"{body['error']['code']}: {body['error']['message']} (request {body.get('request_id')})")

def brl(minor):
    return f"R$ {minor / 100:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")

data = api(f"/connections/{CONN}/summary?group_by=month")["data"]
for m in data["groups"]:
    print(m["label"], "entrou", brl(m["inflow_minor"]), "saiu", brl(m["outflow_minor"]), "sobrou", brl(m["net_minor"]))
print("Saldo hoje:", brl(data["balances_by_currency"].get("BRL", {}).get("balance_minor", 0)))

Para quem mais saiu dinheiro

Problema: Quais fornecedores e contas levam mais dinheiro?

Como: /summary com group_by=counterparty e direction=debit, já ordenado do maior para o menor.

curl
curl -s -H "Authorization: Bearer rpg_test_0000000000000000_demo_public_read_only_sandbox_key_xxxxx" "https://www.rpgcapital.com.br/api/connecta/v1/connections/00000000-0000-4000-8000-0000000000c1/summary?group_by=counterparty&direction=debit"
Node.js 18+ (sem dependências)
// Roda como está (Node 18+, sem dependências) com a chave demo pública.
// Para dados reais: RPG_API_KEY=rpg_live_... RPG_CONNECTION_ID=<uuid> node arquivo.mjs
const BASE = process.env.RPG_API_BASE || 'https://www.rpgcapital.com.br/api/connecta/v1'
const KEY = process.env.RPG_API_KEY || 'rpg_test_0000000000000000_demo_public_read_only_sandbox_key_xxxxx'
const CONN = process.env.RPG_CONNECTION_ID || '00000000-0000-4000-8000-0000000000c1'
async function api(path) {
  const r = await fetch(BASE + path, { headers: { Authorization: 'Bearer ' + KEY } })
  const body = await r.json()
  if (!r.ok) throw new Error(body.error?.code + ': ' + body.error?.message + ' (request ' + body.request_id + ')')
  return body
}
const brl = (minor) => (minor / 100).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })

const { data } = await api('/connections/' + CONN + '/summary?group_by=counterparty&direction=debit')
for (const g of data.groups.slice(0, 10)) console.log(brl(g.outflow_minor).padStart(16), String(g.count).padStart(4) + 'x', g.label, g.key.startsWith('name:') ? '' : '(' + g.key + ')')
Python 3.9+ (sem dependências)
# Roda como está (Python 3.9+, sem dependências) com a chave demo pública.
# Para dados reais: RPG_API_KEY=rpg_live_... RPG_CONNECTION_ID=<uuid> python arquivo.py
import json, os, urllib.request, urllib.error
BASE = os.environ.get("RPG_API_BASE", "https://www.rpgcapital.com.br/api/connecta/v1")
KEY = os.environ.get("RPG_API_KEY", "rpg_test_0000000000000000_demo_public_read_only_sandbox_key_xxxxx")
CONN = os.environ.get("RPG_CONNECTION_ID", "00000000-0000-4000-8000-0000000000c1")

def api(path):
    req = urllib.request.Request(BASE + path, headers={"Authorization": "Bearer " + KEY})
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return json.load(r)
    except urllib.error.HTTPError as e:
        body = json.load(e)
        raise SystemExit(f"{body['error']['code']}: {body['error']['message']} (request {body.get('request_id')})")

def brl(minor):
    return f"R$ {minor / 100:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")

data = api(f"/connections/{CONN}/summary?group_by=counterparty&direction=debit")["data"]
for g in data["groups"][:10]:
    print(brl(g["outflow_minor"]).rjust(16), f"{g['count']:>4}x", g["label"])

Todos os pagamentos para um CPF/CNPJ

Problema: Quanto e quando eu paguei a este fornecedor?

Como: Filtro counterparty_tax_id (pontuação é ignorada) + paginação até o fim.

curl
curl -s -H "Authorization: Bearer rpg_test_0000000000000000_demo_public_read_only_sandbox_key_xxxxx" "https://www.rpgcapital.com.br/api/connecta/v1/connections/00000000-0000-4000-8000-0000000000c1/transactions?counterparty_tax_id=90000000000202&limit=500"
Node.js 18+ (sem dependências)
// Roda como está (Node 18+, sem dependências) com a chave demo pública.
// Para dados reais: RPG_API_KEY=rpg_live_... RPG_CONNECTION_ID=<uuid> node arquivo.mjs
const BASE = process.env.RPG_API_BASE || 'https://www.rpgcapital.com.br/api/connecta/v1'
const KEY = process.env.RPG_API_KEY || 'rpg_test_0000000000000000_demo_public_read_only_sandbox_key_xxxxx'
const CONN = process.env.RPG_CONNECTION_ID || '00000000-0000-4000-8000-0000000000c1'
async function api(path) {
  const r = await fetch(BASE + path, { headers: { Authorization: 'Bearer ' + KEY } })
  const body = await r.json()
  if (!r.ok) throw new Error(body.error?.code + ': ' + body.error?.message + ' (request ' + body.request_id + ')')
  return body
}
const brl = (minor) => (minor / 100).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
async function all(path) {
  // Segue pagination.next_offset até o fim (páginas de até 500)
  const rows = []
  let offset = 0
  while (offset !== null) {
    const sep = path.includes('?') ? '&' : '?'
    const page = await api(path + sep + 'limit=500&offset=' + offset)
    rows.push(...page.data)
    offset = page.pagination.next_offset
  }
  return rows
}

const cnpj = process.argv[2] || '90000000000202'
const rows = await all('/connections/' + CONN + '/transactions?counterparty_tax_id=' + cnpj + '&sort=date_asc')
for (const t of rows) console.log(t.date.slice(0, 10), brl(t.amount_minor), t.operation_type, t.description)
console.log(rows.length, 'transações, total', brl(rows.reduce((n, t) => n + t.amount_minor, 0)))
Python 3.9+ (sem dependências)
# Roda como está (Python 3.9+, sem dependências) com a chave demo pública.
# Para dados reais: RPG_API_KEY=rpg_live_... RPG_CONNECTION_ID=<uuid> python arquivo.py
import json, os, urllib.request, urllib.error
BASE = os.environ.get("RPG_API_BASE", "https://www.rpgcapital.com.br/api/connecta/v1")
KEY = os.environ.get("RPG_API_KEY", "rpg_test_0000000000000000_demo_public_read_only_sandbox_key_xxxxx")
CONN = os.environ.get("RPG_CONNECTION_ID", "00000000-0000-4000-8000-0000000000c1")

def api(path):
    req = urllib.request.Request(BASE + path, headers={"Authorization": "Bearer " + KEY})
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return json.load(r)
    except urllib.error.HTTPError as e:
        body = json.load(e)
        raise SystemExit(f"{body['error']['code']}: {body['error']['message']} (request {body.get('request_id')})")

def brl(minor):
    return f"R$ {minor / 100:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")

import sys
cnpj = sys.argv[1] if len(sys.argv) > 1 else "90000000000202"
rows, offset = [], 0
while offset is not None:
    page = api(f"/connections/{CONN}/transactions?counterparty_tax_id={cnpj}&sort=date_asc&limit=500&offset={offset}")
    rows += page["data"]
    offset = page["pagination"]["next_offset"]
for t in rows:
    print(t["date"][:10], brl(t["amount_minor"]), t["operation_type"], t["description"])
print(len(rows), "transações, total", brl(sum(t["amount_minor"] for t in rows)))

Achar contas fixas e recorrentes

Problema: Quais saídas se repetem todo mês (aluguel, energia, salário)?

Como: Baixa as saídas, agrupa por contraparte e marca quem aparece em 2 ou mais meses diferentes.

Node.js 18+ (sem dependências)
// Roda como está (Node 18+, sem dependências) com a chave demo pública.
// Para dados reais: RPG_API_KEY=rpg_live_... RPG_CONNECTION_ID=<uuid> node arquivo.mjs
const BASE = process.env.RPG_API_BASE || 'https://www.rpgcapital.com.br/api/connecta/v1'
const KEY = process.env.RPG_API_KEY || 'rpg_test_0000000000000000_demo_public_read_only_sandbox_key_xxxxx'
const CONN = process.env.RPG_CONNECTION_ID || '00000000-0000-4000-8000-0000000000c1'
async function api(path) {
  const r = await fetch(BASE + path, { headers: { Authorization: 'Bearer ' + KEY } })
  const body = await r.json()
  if (!r.ok) throw new Error(body.error?.code + ': ' + body.error?.message + ' (request ' + body.request_id + ')')
  return body
}
const brl = (minor) => (minor / 100).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
async function all(path) {
  // Segue pagination.next_offset até o fim (páginas de até 500)
  const rows = []
  let offset = 0
  while (offset !== null) {
    const sep = path.includes('?') ? '&' : '?'
    const page = await api(path + sep + 'limit=500&offset=' + offset)
    rows.push(...page.data)
    offset = page.pagination.next_offset
  }
  return rows
}

const rows = await all('/connections/' + CONN + '/transactions?direction=debit&status=POSTED')
const by = new Map()
for (const t of rows) {
  const who = t.counterparty?.tax_id || t.counterparty?.name || t.description
  const e = by.get(who) || { name: t.counterparty?.name || t.description, months: new Set(), total: 0, n: 0 }
  e.months.add(t.date.slice(0, 7)); e.total += t.amount_minor; e.n++
  by.set(who, e)
}
const fixed = [...by.values()].filter(e => e.months.size >= 2 && e.n <= e.months.size * 2).sort((a, b) => a.total - b.total)
for (const e of fixed) console.log(e.name.padEnd(42), 'média', brl(Math.round(e.total / e.n)), 'em', e.months.size, 'meses')
Python 3.9+ (sem dependências)
# Roda como está (Python 3.9+, sem dependências) com a chave demo pública.
# Para dados reais: RPG_API_KEY=rpg_live_... RPG_CONNECTION_ID=<uuid> python arquivo.py
import json, os, urllib.request, urllib.error
BASE = os.environ.get("RPG_API_BASE", "https://www.rpgcapital.com.br/api/connecta/v1")
KEY = os.environ.get("RPG_API_KEY", "rpg_test_0000000000000000_demo_public_read_only_sandbox_key_xxxxx")
CONN = os.environ.get("RPG_CONNECTION_ID", "00000000-0000-4000-8000-0000000000c1")

def api(path):
    req = urllib.request.Request(BASE + path, headers={"Authorization": "Bearer " + KEY})
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return json.load(r)
    except urllib.error.HTTPError as e:
        body = json.load(e)
        raise SystemExit(f"{body['error']['code']}: {body['error']['message']} (request {body.get('request_id')})")

def brl(minor):
    return f"R$ {minor / 100:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")

from collections import defaultdict
rows, offset = [], 0
while offset is not None:
    page = api(f"/connections/{CONN}/transactions?direction=debit&status=POSTED&limit=500&offset={offset}")
    rows += page["data"]
    offset = page["pagination"]["next_offset"]
by = defaultdict(lambda: {"name": "", "months": set(), "total": 0, "n": 0})
for t in rows:
    cp = t.get("counterparty") or {}
    e = by[cp.get("tax_id") or cp.get("name") or t["description"]]
    e["name"] = cp.get("name") or t["description"]
    e["months"].add(t["date"][:7]); e["total"] += t["amount_minor"]; e["n"] += 1
for e in sorted(by.values(), key=lambda e: e["total"]):
    if len(e["months"]) >= 2 and e["n"] <= len(e["months"]) * 2:
        print(e["name"].ljust(42), "média", brl(round(e["total"] / e["n"])), "em", len(e["months"]), "meses")

Quantos dias de caixa sobram

Problema: Se as vendas pararem, o saldo dura quantos dias?

Como: Saldo das contas de depósito ÷ média diária de saídas dos últimos 60 dias (cartão de crédito fica fora do saldo).

Node.js 18+ (sem dependências)
// Roda como está (Node 18+, sem dependências) com a chave demo pública.
// Para dados reais: RPG_API_KEY=rpg_live_... RPG_CONNECTION_ID=<uuid> node arquivo.mjs
const BASE = process.env.RPG_API_BASE || 'https://www.rpgcapital.com.br/api/connecta/v1'
const KEY = process.env.RPG_API_KEY || 'rpg_test_0000000000000000_demo_public_read_only_sandbox_key_xxxxx'
const CONN = process.env.RPG_CONNECTION_ID || '00000000-0000-4000-8000-0000000000c1'
async function api(path) {
  const r = await fetch(BASE + path, { headers: { Authorization: 'Bearer ' + KEY } })
  const body = await r.json()
  if (!r.ok) throw new Error(body.error?.code + ': ' + body.error?.message + ' (request ' + body.request_id + ')')
  return body
}
const brl = (minor) => (minor / 100).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })

const { data: all } = await api('/connections/' + CONN + '/summary?group_by=month')
const to = all.available_range.to, from = new Date(Date.parse(to) - 59 * 86400000).toISOString().slice(0, 10)
const { data } = await api('/connections/' + CONN + '/summary?group_by=day&from=' + from + '&to=' + to)
const out = data.totals_by_currency.BRL?.outflow_minor ?? 0
const daily = out / 60
const balance = data.balances_by_currency.BRL?.balance_minor ?? 0
console.log('Saldo:', brl(balance), '| saída média/dia:', brl(Math.round(daily)), '| fôlego:', daily ? Math.floor(balance / daily) + ' dias' : 'sem saídas')
Python 3.9+ (sem dependências)
# Roda como está (Python 3.9+, sem dependências) com a chave demo pública.
# Para dados reais: RPG_API_KEY=rpg_live_... RPG_CONNECTION_ID=<uuid> python arquivo.py
import json, os, urllib.request, urllib.error
BASE = os.environ.get("RPG_API_BASE", "https://www.rpgcapital.com.br/api/connecta/v1")
KEY = os.environ.get("RPG_API_KEY", "rpg_test_0000000000000000_demo_public_read_only_sandbox_key_xxxxx")
CONN = os.environ.get("RPG_CONNECTION_ID", "00000000-0000-4000-8000-0000000000c1")

def api(path):
    req = urllib.request.Request(BASE + path, headers={"Authorization": "Bearer " + KEY})
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return json.load(r)
    except urllib.error.HTTPError as e:
        body = json.load(e)
        raise SystemExit(f"{body['error']['code']}: {body['error']['message']} (request {body.get('request_id')})")

def brl(minor):
    return f"R$ {minor / 100:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")

from datetime import date, timedelta
rng = api(f"/connections/{CONN}/summary?group_by=month")["data"]["available_range"]
to = date.fromisoformat(rng["to"]); frm = to - timedelta(days=59)
data = api(f"/connections/{CONN}/summary?group_by=day&from={frm}&to={to}")["data"]
daily = data["totals_by_currency"].get("BRL", {}).get("outflow_minor", 0) / 60
balance = data["balances_by_currency"].get("BRL", {}).get("balance_minor", 0)
print("Saldo:", brl(balance), "| saída média/dia:", brl(round(daily)), "| fôlego:", f"{int(balance // daily)} dias" if daily else "sem saídas")

Buscar por texto, valor e período

Problema: Ache a transação: "aluguel acima de R$ 1.000 em outubro".

Como: q, min_amount_minor (valor absoluto em centavos), from e to combinados.

curl
curl -s -G -H "Authorization: Bearer rpg_test_0000000000000000_demo_public_read_only_sandbox_key_xxxxx" "https://www.rpgcapital.com.br/api/connecta/v1/connections/00000000-0000-4000-8000-0000000000c1/transactions" --data-urlencode "q=aluguel" --data-urlencode "min_amount_minor=100000"
Node.js 18+ (sem dependências)
// Roda como está (Node 18+, sem dependências) com a chave demo pública.
// Para dados reais: RPG_API_KEY=rpg_live_... RPG_CONNECTION_ID=<uuid> node arquivo.mjs
const BASE = process.env.RPG_API_BASE || 'https://www.rpgcapital.com.br/api/connecta/v1'
const KEY = process.env.RPG_API_KEY || 'rpg_test_0000000000000000_demo_public_read_only_sandbox_key_xxxxx'
const CONN = process.env.RPG_CONNECTION_ID || '00000000-0000-4000-8000-0000000000c1'
async function api(path) {
  const r = await fetch(BASE + path, { headers: { Authorization: 'Bearer ' + KEY } })
  const body = await r.json()
  if (!r.ok) throw new Error(body.error?.code + ': ' + body.error?.message + ' (request ' + body.request_id + ')')
  return body
}
const brl = (minor) => (minor / 100).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })

const params = new URLSearchParams({ q: process.argv[2] || 'aluguel', min_amount_minor: '100000', sort: 'amount_desc' })
const page = await api('/connections/' + CONN + '/transactions?' + params)
console.log(page.pagination.total, 'resultados. Dados existem de', page.available_range.from, 'a', page.available_range.to)
for (const t of page.data) console.log(t.date.slice(0, 10), brl(t.amount_minor), t.counterparty?.name || '-', '|', t.description)

Login de agente: pegar uma chave própria

Problema: Meu agente precisa de uma chave sem eu abrir console.

Como: Device login: o script mostra um link, você aprova com Google, o script recebe a chave e grava em .env.

Node.js 18+ (sem dependências)
// Node 18+. Uso: node login.mjs [sandbox|production]
const BASE = process.env.RPG_API_BASE || 'https://www.rpgcapital.com.br/api/connecta/v1'
const environment = process.argv[2] || 'sandbox'
const post = (p, b) => fetch(BASE + p, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(b) }).then(async r => ({ ok: r.ok, body: await r.json() }))
const start = await post('/auth/device', { environment, client_name: 'Script de login' })
if (!start.ok) throw new Error(start.body.error.message)
console.log('Abra e aprove:', start.body.verification_uri_complete, '| código', start.body.user_code)
let wait = start.body.interval
for (;;) {
  await new Promise(r => setTimeout(r, wait * 1000))
  const r = await post('/auth/token', { device_code: start.body.device_code })
  if (r.ok) {
    const { appendFileSync } = await import('node:fs')
    appendFileSync('.env', 'RPG_API_KEY=' + r.body.api_key + '\n')
    console.log('Chave', r.body.environment, 'gravada em .env')
    break
  }
  if (r.body.error.code === 'slow_down') wait += 5
  else if (r.body.error.code !== 'authorization_pending') throw new Error(r.body.error.code + ': ' + r.body.error.message)
}

Conectar um banco real (produção)

Problema: Meu cliente precisa autorizar o banco dele.

Como: Cria a conexão, mostra connect_url ao titular (vale 30 min) e espera o status virar connected. Exige chave rpg_live_.

Node.js 18+ (sem dependências)
// Node 18+. RPG_API_KEY=rpg_live_... node conectar.mjs
const BASE = process.env.RPG_API_BASE || 'https://www.rpgcapital.com.br/api/connecta/v1'
const KEY = process.env.RPG_API_KEY
if (!KEY?.startsWith('rpg_live_')) throw new Error('Use uma chave de produção (rpg_live_). Pegue com a receita login-de-agente production.')
const call = (method, path, body) => fetch(BASE + path, { method, headers: { Authorization: 'Bearer ' + KEY, 'content-type': 'application/json' }, body: body && JSON.stringify(body) }).then(r => r.json())
const created = await call('POST', '/connections', { provider: 'open_finance' })
if (created.error) throw new Error(created.error.code + ': ' + created.error.message)
console.log('Envie ao titular da conta:', created.connect_url)
for (let i = 0; i < 120; i++) {
  await new Promise(r => setTimeout(r, 5000))
  const { data } = await call('GET', '/connections/' + created.data.id)
  if (['connected', 'partial'].includes(data.status)) { console.log('Conectado:', data.name, data.counts); break }
  if (['action_required', 'unavailable', 'error'].includes(data.status)) throw new Error('Status ' + data.status + ' (' + data.error_code + ')')
}

App com vários clientes: cada um conecta o próprio banco

Problema: Meu SaaS precisa que cada cliente conecte o banco dele e volte para o meu app.

Como: Seu servidor cria a conexão com redirect_url e external_id e manda o cliente para connect_url. Ele aceita o termo, autoriza no banco e volta para o seu app com connection_id e status. Guarde connection_id junto do cliente. Exige chave rpg_live_.

Node.js 18+ (sem dependências)
// Node 18+, sem dependências. RPG_API_KEY=rpg_live_... node app.mjs  → abra http://localhost:3000/conectar?cliente=123
import { createServer } from 'node:http'
const BASE = process.env.RPG_API_BASE || 'https://www.rpgcapital.com.br/api/connecta/v1'
const KEY = process.env.RPG_API_KEY
const conexoes = new Map() // troque pelo seu banco de dados: cliente → connection_id
createServer(async (req, res) => {
  const url = new URL(req.url, 'http://localhost:3000')
  if (url.pathname === '/conectar') {
    const cliente = url.searchParams.get('cliente') || 'demo'
    const r = await fetch(BASE + '/connections', { method: 'POST', headers: { Authorization: 'Bearer ' + KEY, 'content-type': 'application/json' },
      body: JSON.stringify({ provider: 'open_finance', external_id: cliente, app_name: 'Meu App', redirect_url: 'http://localhost:3000/banco-conectado' }) })
    const body = await r.json()
    if (!r.ok) { res.writeHead(r.status).end(body.error.message); return }
    conexoes.set(cliente, body.data.id)
    res.writeHead(302, { location: body.connect_url }).end()  // o cliente vai para a tela da RPG e depois para o banco
    return
  }
  if (url.pathname === '/banco-conectado') {
    const { connection_id, status, external_id } = Object.fromEntries(url.searchParams)
    // status: connected | partial | pending | error. Confirme sempre no servidor antes de confiar:
    const r = await fetch(BASE + '/connections/' + connection_id, { headers: { Authorization: 'Bearer ' + KEY } })
    const { data } = await r.json()
    res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' }).end('Cliente ' + external_id + ': ' + status + ' (servidor diz ' + data?.status + ', ' + (data?.counts?.transactions ?? 0) + ' transações)')
    return
  }
  res.writeHead(404).end()
}).listen(3000, () => console.log('http://localhost:3000/conectar?cliente=123'))

Receber webhooks com assinatura conferida

Problema: Quero ser avisado quando a conexão atualizar, sem fazer polling.

Como: Configure com PUT /webhook {url}. Confira RPG-Signature: HMAC-SHA256 de "t.corpo_bruto" com o signing_secret e rejeite se tiver mais de 5 minutos.

Node.js 18+ (sem dependências)
// Node 18+. RPG_WEBHOOK_SECRET=rpgwh_... node webhook.mjs
import { createServer } from 'node:http'
import { createHmac, timingSafeEqual } from 'node:crypto'
const SECRET = process.env.RPG_WEBHOOK_SECRET
function valid(raw, header) {
  const parts = Object.fromEntries(String(header).split(',').map(p => p.split('=')))
  if (!parts.t || Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false
  const expected = createHmac('sha256', SECRET).update(parts.t + '.' + raw).digest()
  const got = Buffer.from(parts.v1 || '', 'hex')
  return got.length === expected.length && timingSafeEqual(got, expected)
}
createServer((req, res) => {
  let raw = ''
  req.on('data', c => raw += c)
  req.on('end', () => {
    if (!valid(raw, req.headers['rpg-signature'])) { res.writeHead(401).end(); return }
    const event = JSON.parse(raw)
    console.log(event.type, event.connection_id, event.data)
    res.writeHead(200).end('ok')
  })
}).listen(3000, () => console.log('Ouvindo em :3000'))

Next.js: rota de servidor com a chave escondida

Problema: Quero mostrar o caixa no meu app sem expor a chave no navegador.

Como: Route Handler no servidor chama o Connecta; o front só chama /api/caixa.

app/api/caixa/route.ts
// app/api/caixa/route.ts — a chave fica só no servidor (RPG_API_KEY em .env.local)
const BASE = 'https://www.rpgcapital.com.br/api/connecta/v1'
export async function GET() {
  const key = process.env.RPG_API_KEY ?? 'rpg_test_0000000000000000_demo_public_read_only_sandbox_key_xxxxx'
  const conn = process.env.RPG_CONNECTION_ID ?? '00000000-0000-4000-8000-0000000000c1'
  const r = await fetch(BASE + '/connections/' + conn + '/summary?group_by=month', { headers: { Authorization: 'Bearer ' + key }, cache: 'no-store' })
  const body = await r.json()
  if (!r.ok) return Response.json({ error: body.error?.message ?? 'Falha' }, { status: r.status })
  return Response.json({ meses: body.data.groups, saldo: body.data.balances_by_currency.BRL ?? null, sintetico: body.meta.synthetic })
}
BALCÃO · v12.0 · 8c3d8c3