# Build your first financial integration.

The complete RPG Connecta guide: API keys, sandbox, bank consent, data, webhooks and production. Public documentation for developers and agents.

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

## From account to first response

Sign in with Google in the console. Choose Sandbox, create a named API key and save its full value immediately: it is shown only once. No application registration, website URL or Malvo account is required.

Store the key as RPG_API_KEY on your backend. Create one synthetic connection, save data.id as RPG_CONNECTION_ID, then read accounts and transactions. The examples below run outside the console; console queries instead use your signed-in session.

- 1. Create an RPG sandbox key.
- 2. Run a quickstart and retain its connection ID.
- 3. Explore JSON, switch scenarios and inspect events.

```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"
```

## Keys, permissions and environments

Send Authorization: Bearer followed by your RPG key on every API call. rpg_test_ identifies sandbox; rpg_live_ identifies production. Optional X-RPG-Environment must match the key. A credential can access only its owner’s connections in its own environment.

Keep secrets in server environment variables or a secret manager. Never place them in browser bundles, public prompts, logs or Git. Rename keys in the console; rotate by creating a replacement, updating your backend and revoking the old key. Revocation takes effect immediately.

- Limit: 10 active keys per user per environment.
- Limit: 60 requests per minute per key. Respect Retry-After on HTTP 429.
- Limit: 20 active connections per user per environment.

## A complete sandbox inside your workspace

The RPG simulator persists synthetic accounts and transactions per connection. Use provider=rpg_simulator, then POST /connections/{id}/simulate with a scenario. It does not contact a bank, require a card or create a real payment.

success produces a complete fixture; pending awaits authorization; expired requires renewed consent; unavailable simulates an institution outage; partial exposes an incomplete snapshot. Previously valid data can remain visible during an outage or pending state: inspect status and last_synced_at before using it.

The dedicated Malvo sandbox is a separate adapter that requires RPG-side sandbox credentials. Its readiness is displayed in advanced options. The anonymous public preview at /developers/sandbox/data is a separate small fixture, not your persistent workspace.

- Use success after another scenario to restore the complete synthetic fixture.
- Reuse connection IDs when rerunning examples; disconnect unused fixtures.

## Production and bank consent

Explicitly activate Production in the console, create a production key and POST /connections with {"provider":"malvo"}. RPG authenticates with its own Malvo account server-side. The response includes an ephemeral connect_url for the account holder to authorize bank access. Do not log it or let agents automatically open or preview it.

A bank connection belongs to the developer’s RPG account. In an end-user integration, your backend must map that connection to the correct signed-in customer and enforce access before returning data. Never give an RPG API key to the customer’s browser.

After authorization, your backend calls POST /connections/{id}/complete with {"item_id":"returned item ID"}. RPG verifies provider ownership. The hosted /developers/connect return page can complete the flow for a signed-in developer; an end-user journey must use your own authenticated backend completion flow.

Read GET /connections/{id} for status. POST /sync reads the latest data available at the provider; it does not guarantee a new bank refresh. POST /reconnect returns a new consent URL. DELETE /connections/{id} disconnects and removes the local snapshot.

- Production uses real data and requires the account holder’s consent.
- Gateway billing is not enabled; activation does not charge a card.
- Bank availability and financial coverage depend on the provider and institution.

## A predictable data contract

Responses use JSON. Resource lists have data, pagination and meta. Monetary values are integer minor units: 185000 BRL means R$ 1,850.00. Preserve currency when displaying or aggregating amounts. Positive transaction amounts represent incoming funds; negative amounts represent outgoing funds.

IDs are source identifiers, not labels; retain them for reconciliation. Categories can be a string, provider object or null. Tags may be absent upstream; never infer that missing classification means zero activity. Dates and missing fields should be handled explicitly.

The response below is illustrative. Your actual IDs, dates, categories, records and balances may differ. Current resources cover accounts and transactions; investments, credit reports, cards and payments are not additional published Connecta resources.



```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}
}
```

## Pagination, partial results and freshness

Accounts and transactions accept offset (a non-negative integer). Each page contains up to 100 records. Follow pagination.next_offset until null; transactions also accepts account_id. Do not assume the first page is the entire history.

meta.partial and meta.status describe the snapshot. Always expose last_synced_at and an incomplete-data indicator in your product. A successful HTTP response can contain stale or partial data. Bank refresh latency is not a real-time balance guarantee.

Provider ingestion is bounded; large histories may be partial. Polling sync repeatedly does not remove those limits. Events and request feeds expose only the latest 100 entries; the console shows a smaller recent history. Persist the history your own product needs.



## Complete examples you can run

Use Node.js 18+ or Python 3 with the standard library; no SDK installation is required. Export RPG_API_KEY in your terminal using your own sandbox key. Save either example as the filename in its first comment, then run it.

Set RPG_CONNECTION_ID to reuse a fixture on subsequent runs. Both examples create a synthetic connection only when that variable is absent, then read accounts and paginate transactions. A failed call includes server error details for diagnosis.



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

## Signed webhooks and safe processing

Configure a public HTTPS endpoint in the console for each environment. Copy the signing secret when it is displayed. The header RPG-Signature has t=<Unix timestamp>,v1=<hex HMAC>. Compute HMAC-SHA256(secret, timestamp + "." + raw request body). Verify the signature before parsing or processing data and reject timestamps more than five minutes away.

Use the event ID as a durable unique key. Persist receipt and enqueue business work atomically; repeated delivery must not repeat financial side effects. The example validates signatures only and intentionally performs no business operation; add durable storage before processing events in production.

Delivery status and manual resend are available in the console. Automatic retries are not enabled. Upstream Malvo automatic events additionally require RPG-side signing-secret configuration; when unavailable, use completion and explicit sync. Treat an event as a signal to read the canonical connection state.



```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));
```

## Errors with a clear recovery path

Errors contain error.code, error.message and request_id; X-Request-ID is also returned. Share the request ID when requesting support, never the API key or a financial payload.

Retry transient failures with bounded exponential backoff and jitter. Respect Retry-After. Do not blindly retry POST /connections after an ambiguous timeout: list existing connections first to avoid duplicates. There is no published idempotency-key contract.



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

## Build a useful dashboard

Start with a server route that reads the authenticated customer’s mapped connection. Fetch all account pages and transaction pages, retain IDs and show balances in the correct currency. Separate incoming and outgoing transactions; group by month and category while keeping unknown categories visible.

Show connection state, latest synchronization time, partial-data status and a reconnect action beside the data. A 401 should offer sign-in or credential repair; a bank outage should preserve the latest valid snapshot with its timestamp. Test all five sandbox states before switching to production.



## Instructions for coding agents

Read this guide and the OpenAPI contract before generating code. Use the English Markdown URL as the compact entry point. Keep placeholders for credentials; ask the user to create a key in the console and place it in the backend secret store.

Generate a server integration, map user ownership, paginate, validate webhook signatures and expose errors with request IDs. Use the sandbox first. Do not invent MCP endpoints, installable skills, SDK packages, bank coverage, pricing or payment capabilities.

An agent may help create code and explain the authorization steps. Google sign-in, bank consent and activation must remain visible to the user and respect the authentication tools and permissions available in the agent’s environment.



## Production readiness and current boundaries

Before launch: rotate development credentials, isolate sandbox and production secrets, verify customer ownership checks, test expired consent and partial results, validate raw-body signatures and durable deduplication, and confirm your monitoring and support workflow.

Current functionality: Google sign-in, RPG keys, persistent simulator, Malvo production adapter, accounts, transactions, events, request diagnostics and manually resent signed webhooks. The dedicated Malvo sandbox and upstream event readiness depend on server configuration.

No published MCP server, packaged SDK, automatic webhook retries or billing gateway is available. The legacy merchant API at /api/public/v1 and /developers/docs has a different contract and must not be used with Connecta keys. Contact: comercial@rpgcapital.com.br.



## Endpoint

- GET /connections: List your connections
- POST /connections: Create a synthetic or bank-consent connection
- GET /connections/{id}: Read connection status
- GET /connections/{id}/accounts: Read account balances; offset pagination
- GET /connections/{id}/transactions: Read transactions; offset and account_id filters
- POST /connections/{id}/simulate: Change the RPG simulator scenario
- POST /connections/{id}/sync: Read the latest provider snapshot
- POST /connections/{id}/complete: Verify and bind the authorized provider item
- POST /connections/{id}/reconnect: Generate a new bank-consent URL
- DELETE /connections/{id}: Disconnect and delete the local snapshot
- GET /events: Read the latest 100 events
- GET /requests: Read the latest 100 request summaries

## Data dictionary

- id: Stable source identifier; keep it for reconciliation
- account_id: Account associated with the transaction
- name / type: Account display name and account type
- balance_minor: Balance in integer minor currency units
- amount_minor: Signed transaction amount in integer minor units
- currency: Explicit ISO currency code; do not mix currencies
- date: Source transaction date; handle missing values
- description: Source transaction description
- category: String, object or null; handle all shapes
- tags: Classification metadata; source coverage varies
- pagination.next_offset: Next offset, or null when there are no more pages
- meta.synthetic / provider: Whether data is fictitious and which provider produced it
- meta.status / partial: Connection state and whether the snapshot is incomplete
- meta.last_synced_at: Snapshot time; expose it in your UI

## Status

- 401 invalid_api_key: Missing, invalid or revoked key. Replace the server credential; never use a session cookie as an API key.
- 403 environment_mismatch: Header and key disagree. Use the matching environment or remove the optional header.
- 403 production_inactive: Production has not been activated. Activate it explicitly in the console.
- 404 not_found: Connection not found for this account and environment. Check the ID, owner and key environment.
- 409 authorization_pending: Bank authorization is incomplete. Complete consent before synchronizing.
- 409 connection_limit / key_limit: Active resource limit reached. Revoke an unused key or disconnect an unused connection.
- 429 rate_limited: Per-key request limit reached. Wait for Retry-After; reduce polling.
- 502 provider_error / provider_auth_error: Provider failed or rejected authentication. Retry transient failures; persistent failures require RPG support.
- 503 provider_not_configured: Provider credentials are unavailable. Use the RPG simulator or contact RPG support.
- 400 invalid_scenario / invalid_offset: Invalid request parameter. Use a supported scenario and a non-negative integer offset.
