# 从 API 密钥到第一份数据。

RPG Connecta 完整指南：API 密钥、沙盒、银行授权、数据结构、Webhook 与生产环境。面向开发者和 AI 代理的公开文档。

API: https://www.rpgcapital.com.br/api/connecta/v1
Console: https://www.rpgcapital.com.br/developers
OpenAPI: https://www.rpgcapital.com.br/developers/connecta-openapi.json

## 从注册到第一次响应

在控制台使用 Google 登录。选择 Sandbox，创建一个有名称的 API 密钥，并立即保存完整值：完整密钥仅显示一次。无需注册应用、填写网站 URL 或拥有 Malvo 账户。

在后端将密钥保存为 RPG_API_KEY。创建一个模拟连接，将 data.id 保存为 RPG_CONNECTION_ID，然后查询账户和交易。下方示例在控制台外运行；控制台内的查询使用已登录会话。

- 1. 创建 RPG 沙盒密钥。
- 2. 运行快速入门示例并保存连接 ID。
- 3. 查看 JSON、切换场景并检查事件。

```bash
# Requires bash, curl and jq. Store your sandbox key in RPG_API_KEY.
set -euo pipefail
: "${RPG_API_KEY:?Set RPG_API_KEY in your shell}"
BASE='https://www.rpgcapital.com.br/api/connecta/v1'
CONNECTION_ID=$(curl --fail-with-body -sS "$BASE/connections" \
  -H "Authorization: Bearer $RPG_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"provider":"rpg_simulator","scenario":"success"}' | jq -er '.data.id')
curl --fail-with-body -sS "$BASE/connections/$CONNECTION_ID/accounts" \
  -H "Authorization: Bearer $RPG_API_KEY"
curl --fail-with-body -sS "$BASE/connections/$CONNECTION_ID/transactions?offset=0" \
  -H "Authorization: Bearer $RPG_API_KEY"
```

## 密钥、权限与环境

每次 API 请求都必须发送 Authorization: Bearer，后接 RPG 密钥。rpg_test_ 表示沙盒，rpg_live_ 表示生产环境。可选请求头 X-RPG-Environment 必须与密钥一致。密钥只能访问其所有者在对应环境中的连接。

将密钥保存在服务端环境变量或密钥管理器中，不要放入浏览器代码、公开提示词、日志或 Git。可在控制台重命名密钥。轮换时先创建新密钥、更新后端，再撤销旧密钥；撤销立即生效。

- 每位用户每个环境最多 10 个有效密钥。
- 每个密钥每分钟最多 60 次请求；HTTP 429 时遵守 Retry-After。
- 每位用户每个环境最多 20 个有效连接。

## 工作区内的完整沙盒

RPG 模拟器按连接持久保存虚构账户和交易。使用 provider=rpg_simulator，再通过 POST /connections/{id}/simulate 切换场景。它不会联系真实银行、要求信用卡或执行真实支付。

success 提供完整模拟数据；pending 等待授权；expired 要求续期；unavailable 模拟银行不可用；partial 提供不完整快照。故障或等待期间可能保留之前有效的数据：务必检查 status 和 last_synced_at。

Malvo 专用沙盒是另一个适配器，需要 RPG 配置专用沙盒凭据。高级选项会显示可用状态。匿名预览 /developers/sandbox/data 是独立的小型样本，不是工作区的持久连接。

- 切回 success 可恢复完整模拟数据。
- 重复运行时复用连接 ID，断开不再使用的连接。

## 生产环境与银行授权

在控制台明确激活 Production，创建生产密钥，然后向 POST /connections 发送 {"provider":"malvo"}。RPG 在服务端使用自己的 Malvo 账户认证。响应中的临时 connect_url 供账户持有人授权银行访问。不要记录此 URL，也不要让代理自动打开或预览它。

银行连接属于开发者的 RPG 账户。面向最终用户时，你的后端必须将连接关联到正确的已登录客户，并在返回数据前验证关联。不要向客户浏览器提供 RPG 密钥。

授权后，由你的后端调用 POST /connections/{id}/complete，提交 {"item_id":"返回的 item ID"}。RPG 会验证提供商侧的所有权。/developers/connect 返回页可为已登录开发者完成流程；最终用户流程应通过你自己的已认证后端完成。

GET /connections/{id} 查询状态；POST /sync 读取提供商已提供的数据，不保证触发新的银行刷新；POST /reconnect 生成新的授权链接；DELETE /connections/{id} 断开连接并删除本地快照。

- 生产环境使用真实数据，必须取得持有人的同意。
- 支付网关尚未启用，激活不会向信用卡收费。
- 银行覆盖范围和可用性取决于提供商与银行。

## 可预测的数据结构

响应为 JSON。资源列表包含 data、pagination 和 meta。金额使用最小货币单位的整数：185000 BRL 表示 1,850.00 巴西雷亚尔。显示和汇总时必须保留货币。正交易金额为流入，负金额为流出。

ID 是来源标识，应保存以便对账。category 可以是字符串、提供商对象或 null。来源可能不提供 tags；缺少分类不表示没有交易活动。明确处理日期和缺失字段。

下方响应仅为示例；实际 ID、日期、分类、记录和余额可能不同。当前公开资源仅包括账户与交易，不包含额外的投资、信用报告、银行卡或支付接口。



```json
{
  "data": [{
    "id": "CONNECTION_ID_tx_1",
    "account_id": "CONNECTION_ID_checking",
    "date": "2026-10-31",
    "description": "Recebimento de cliente",
    "amount_minor": 185000,
    "currency": "BRL",
    "category": "Receitas",
    "tags": ["synthetic", "sandbox"]
  }],
  "pagination": {"offset": 0, "limit": 100, "total": 1, "next_offset": null},
  "meta": {"environment":"sandbox", "provider":"rpg_simulator", "synthetic":true, "status":"connected", "last_synced_at":"2026-10-31T12:00:00Z", "partial":false, "note":null}
}
```

## 分页、不完整结果与数据时效

账户和交易接受非负整数 offset，每页最多 100 条。持续读取 pagination.next_offset，直到值为 null。交易还支持 account_id。不要把第一页当作完整历史。

meta.partial 与 meta.status 描述快照状态。界面必须显示 last_synced_at 和数据不完整提示。HTTP 成功响应仍可能包含旧数据或部分数据；余额不保证实时更新。

提供商采集有上限，大型历史可能不完整。重复调用 sync 不会取消这些限制。事件与请求接口仅提供最近 100 条；控制台显示的近期历史更少。产品所需历史应自行持久保存。



## 可直接运行的完整示例

使用 Node.js 18+ 或 Python 3 标准库，无需安装 SDK。在终端设置自己的沙盒密钥 RPG_API_KEY，按示例首行注释保存文件后运行。

设置 RPG_CONNECTION_ID 可复用已有模拟连接。两个示例仅在缺少该变量时创建连接，然后查询账户并遍历交易分页。失败时会提供服务端返回的错误详情。



```javascript
// Node.js 18+. Save as quickstart.mjs; run: node quickstart.mjs
const base = 'https://www.rpgcapital.com.br/api/connecta/v1';
const key = process.env.RPG_API_KEY;
if (!key?.startsWith('rpg_test_')) throw new Error('Use a sandbox RPG_API_KEY');
async function api(path, body) {
  const response = await fetch(base + path, {
    method: body ? 'POST' : 'GET',
    signal: AbortSignal.timeout(30000),
    headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
    ...(body ? { body: JSON.stringify(body) } : {})
  });
  const payload = await response.json();
  if (!response.ok) throw new Error(`${response.status} ${payload.error?.code}: ${payload.error?.message} [${payload.request_id}]`);
  return payload;
}
// Reuse RPG_CONNECTION_ID on subsequent runs to avoid creating duplicate fixtures.
const id = process.env.RPG_CONNECTION_ID || (await api('/connections', {
  provider: 'rpg_simulator', scenario: 'success'
})).data.id;
console.log('Connection:', id);
console.log('Accounts:', (await api(`/connections/${id}/accounts`)).data);
const transactions = [];
let offset = 0;
do {
  const page = await api(`/connections/${id}/transactions?offset=${offset}`);
  transactions.push(...page.data);
  offset = page.pagination.next_offset;
} while (offset !== null);
console.log('Transactions:', transactions);
```

```python
# Python 3, standard library only. Save as quickstart.py.
import json, os, urllib.request, urllib.error
BASE = 'https://www.rpgcapital.com.br/api/connecta/v1'
KEY = os.environ.get('RPG_API_KEY', '')
if not KEY.startswith('rpg_test_'):
    raise RuntimeError('Use a sandbox RPG_API_KEY')
def api(path, body=None):
    request = urllib.request.Request(BASE + path,
        data=json.dumps(body).encode() if body is not None else None,
        headers={'Authorization': 'Bearer ' + KEY, 'Content-Type': 'application/json'})
    try:
        with urllib.request.urlopen(request, timeout=30) as response:
            return json.load(response)
    except urllib.error.HTTPError as error:
        details = json.load(error)
        raise RuntimeError(f'{error.code}: {details}') from error
connection_id = os.environ.get('RPG_CONNECTION_ID') or api('/connections',
    {'provider': 'rpg_simulator', 'scenario': 'success'})['data']['id']
print('Connection:', connection_id)
print('Accounts:', api(f'/connections/{connection_id}/accounts')['data'])
transactions, offset = [], 0
while offset is not None:
    page = api(f'/connections/{connection_id}/transactions?offset={offset}')
    transactions.extend(page['data'])
    offset = page['pagination']['next_offset']
print('Transactions:', transactions)
```

## 签名 Webhook 与安全处理

为每个环境在控制台配置公开 HTTPS 端点，立即保存显示的签名密钥。RPG-Signature 包含 t=<Unix 时间戳>,v1=<十六进制 HMAC>。计算 HMAC-SHA256(secret, timestamp + "." + 原始请求体)。解析或处理前先验证签名，拒绝时间差超过五分钟的请求。

使用事件 ID 作为持久化唯一键。原子地记录接收结果并将业务工作入队，重复投递不得重复产生金融副作用。示例只验证签名，不执行任何业务操作；生产处理事件前必须添加持久化存储。

控制台提供投递状态和手动重发，不支持自动重试。Malvo 上游自动事件还需 RPG 配置签名密钥；不可用时使用 complete 和显式 sync。将事件视为重新读取连接权威状态的信号。



```javascript
// Node.js 18+, no framework. Terminate HTTPS at your public hosting provider.
import http from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';
const secret = process.env.RPG_WEBHOOK_SECRET;
if (!secret) throw new Error('Set RPG_WEBHOOK_SECRET');
http.createServer(async (req, res) => {
  if (req.method !== 'POST' || req.url !== '/webhooks/rpg') { res.writeHead(404).end(); return; }
  const chunks = []; let size = 0;
  for await (const chunk of req) {
    size += chunk.length;
    if (size > 1_000_000) { res.writeHead(413).end(); return; }
    chunks.push(chunk);
  }
  const raw = Buffer.concat(chunks);
  const parts = String(req.headers['rpg-signature'] || '').split(',');
  const t = parts.find(p => p.startsWith('t='))?.slice(2) || '';
  if (!/^\d+$/.test(t) || Math.abs(Date.now()/1000 - Number(t)) > 300) { res.writeHead(401).end(); return; }
  const expected = createHmac('sha256', secret).update(t + '.').update(raw).digest();
  const valid = parts.filter(p => p.startsWith('v1=')).some(p => {
    const hex = p.slice(3); if (!/^[a-f0-9]{64}$/.test(hex)) return false;
    return timingSafeEqual(Buffer.from(hex, 'hex'), expected);
  });
  if (!valid) { res.writeHead(401).end(); return; }
  try {
    const event = JSON.parse(raw.toString('utf8'));
    // REQUIRED: persist event.id with a UNIQUE constraint in your database,
    // and enqueue the business effect in the same transaction (outbox pattern).
    // Duplicate event IDs must become a no-op, including after restarts.
    // This signature example intentionally performs no business effect.
    res.writeHead(200, {'Content-Type':'application/json'}).end(JSON.stringify({received:true,id:event.id}));
  } catch { res.writeHead(400).end(); }
}).listen(Number(process.env.PORT || 3000));
```

## 有明确恢复路径的错误

错误包含 error.code、error.message 和 request_id，同时返回 X-Request-ID。联系支持时提供请求 ID，不要提供 API 密钥或金融数据。

瞬时故障应采用有上限的指数退避与随机抖动，并遵守 Retry-After。POST /connections 超时且结果不明时，先列出连接避免重复创建，不要盲目重试。目前未发布幂等键协议。



```json
{
  "error": {
    "code": "environment_mismatch",
    "message": "Esta chave pertence a outro ambiente."
  },
  "request_id": "uuid"
}
```

## 构建实用的数据看板

首先编写服务端路由，读取与已登录客户关联的连接。遍历账户和交易分页，保留 ID，按货币展示余额。分别显示流入与流出，按月份和分类汇总，并保留未知分类。

在数据旁显示连接状态、最近同步时间、部分数据提示和重新授权操作。401 应提供登录或凭据修复路径。银行故障时保留带时间戳的有效快照。上线前测试全部五种沙盒状态。



## 面向编程代理的说明

生成代码前先阅读本指南与 OpenAPI。可将英文 Markdown 地址作为精简入口。凭据使用占位符，引导用户在控制台创建密钥并将其存入后端密钥管理器。

生成服务端集成、客户所有权映射、分页、签名验证和含 request ID 的错误处理。先用沙盒。不要虚构 MCP 端点、可安装技能、SDK 包、银行覆盖、价格或支付功能。

代理可以生成代码并解释授权步骤。Google 登录、银行同意和激活必须对用户可见，并遵守代理环境可用的认证工具和权限。



## 生产就绪检查与当前边界

上线前轮换开发凭据、隔离各环境密钥、验证客户所有权、测试授权过期和部分数据、验证原始请求体签名及持久去重，并确认监控和支持流程。

已提供：Google 登录、RPG 密钥、持久模拟器、Malvo 生产适配器、账户、交易、事件、请求诊断，以及可手动重发的签名 Webhook。Malvo 专用沙盒与上游自动事件取决于服务端配置。

尚未提供公开 MCP 服务、打包 SDK、自动 Webhook 重试或支付网关。旧商户 API /api/public/v1 和 /developers/docs 使用不同协议，不能使用 Connecta 密钥。联系邮箱：comercial@rpgcapital.com.br。



## 端点

- GET /connections: 列出你的连接
- POST /connections: 创建模拟连接或银行授权连接
- GET /connections/{id}: 读取连接状态
- GET /connections/{id}/accounts: 读取账户余额；支持 offset 分页
- GET /connections/{id}/transactions: 读取交易；支持 offset 与 account_id
- POST /connections/{id}/simulate: 切换 RPG 模拟场景
- POST /connections/{id}/sync: 读取提供商最新快照
- POST /connections/{id}/complete: 验证并绑定已授权 item
- POST /connections/{id}/reconnect: 生成新的银行授权链接
- DELETE /connections/{id}: 断开连接并删除本地快照
- GET /events: 读取最近 100 个事件
- GET /requests: 读取最近 100 条请求摘要

## 数据字典

- id: 稳定的来源 ID，用于对账
- account_id: 交易关联的账户
- name / type: 账户显示名称与类型
- balance_minor: 最小货币单位的整数余额
- amount_minor: 带正负号的最小单位整数金额
- currency: 明确货币代码；不同货币不要混合
- date: 来源交易日期；处理缺失值
- description: 来源交易描述
- category: 字符串、对象或 null
- tags: 分类元数据；来源覆盖不同
- pagination.next_offset: 下一页 offset，结束时为 null
- meta.synthetic / provider: 是否为虚构数据及其提供商
- meta.status / partial: 连接状态及快照是否不完整
- meta.last_synced_at: 快照时间；应在界面显示

## 状态

- 401 invalid_api_key: 密钥缺失、无效或已撤销. 更换服务端凭据；会话 Cookie 不是 API 密钥.
- 403 environment_mismatch: 请求头与密钥环境不一致. 使用对应环境或移除可选请求头.
- 403 production_inactive: 生产环境尚未激活. 在控制台明确激活.
- 404 not_found: 此账户和环境中找不到连接. 检查 ID、所有者与密钥环境.
- 409 authorization_pending: 银行授权尚未完成. 完成授权后再同步.
- 409 connection_limit / key_limit: 有效资源数量达到上限. 撤销闲置密钥或断开闲置连接.
- 429 rate_limited: 超过每密钥请求速率. 等待 Retry-After 并减少轮询.
- 502 provider_error / provider_auth_error: 提供商故障或拒绝认证. 瞬时错误可重试；持续故障请联系 RPG.
- 503 provider_not_configured: 提供商凭据未配置. 使用 RPG 模拟器或联系支持.
- 400 invalid_scenario / invalid_offset: 请求参数无效. 使用支持的场景及非负整数 offset.
