// 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 })
}