# Exemplos executáveis

Fonte: https://docs.ruralzap.com.br/exemplos

Código de consulta e downloads gerados do mesmo contrato da referência.





Configure RURALZAP_BASE_URL e RURALZAP_API_KEY no servidor. Nunca cole chaves em prompts ou código público. Exemplos de chamada única não implementam paginação ou retentativas; use os leitores completos abaixo para percorrer lançamentos.

[OpenAPI](/openapi.json) · [Postman](/postman.json)

* Consultar contexto da integração: [cURL](/examples/context.sh), [TypeScript](/examples/context.ts), [Python](/examples/context.py).
* Listar carteiras ativas: [cURL](/examples/wallets.sh), [TypeScript](/examples/wallets.ts), [Python](/examples/wallets.py).
* Consultar saldos correntes: [cURL](/examples/balances.sh), [TypeScript](/examples/balances.ts), [Python](/examples/balances.py).
* Consultar lançamentos por período: [cURL](/examples/transactions.sh), [TypeScript](/examples/transactions.ts), [Python](/examples/transactions.py).
* Consultar resumo financeiro realizado: [cURL](/examples/reports-summary.sh), [TypeScript](/examples/reports-summary.ts), [Python](/examples/reports-summary.py).
* Consultar fluxo de caixa realizado: [cURL](/examples/reports-cashflow.sh), [TypeScript](/examples/reports-cashflow.ts), [Python](/examples/reports-cashflow.py).

## Leitor completo em TypeScript [#leitor-completo-em-typescript]

[Baixar read-transactions.ts](/examples/read-transactions.ts)

```typescript
/**
 * RuralZap — exemplo executável no servidor com Node.js 22+ e tsx.
 * npx --yes tsx read-transactions.ts 2026-01-01 2026-01-31
 * Configure RURALZAP_BASE_URL (origem) e RURALZAP_API_KEY no ambiente privado.
 * Não execute este arquivo no navegador nem registre respostas financeiras.
 */
import process from "node:process";
import { setTimeout as sleep } from "node:timers/promises";

const MAX_ATTEMPTS = 4;
const MAX_PAGES = 10_000;
const TIMEOUT_MS = 30_000;
const MONEY = /^-?\d+\.\d{4}$/;

class IntegrationError extends Error {}

type Transaction = {
  id: string;
  amount: string;
  currency: string;
  date: string;
};

type TransactionPage = {
  data: Transaction[];
  meta: { hasMore: boolean; nextCursor: string | null };
};

function isObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function dateOnlyMilliseconds(value: string): number {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
    throw new IntegrationError("Use datas no formato YYYY-MM-DD.");
  }
  const [year, month, day] = value.split("-").map(Number);
  const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
  const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  if (
    year < 100 ||
    month < 1 ||
    month > 12 ||
    day < 1 ||
    day > days[month - 1]
  ) {
    throw new IntegrationError("Data de calendário inválida.");
  }
  // Somente para comparar o intervalo. Enviamos as strings originais sem fuso.
  return Date.UTC(year, month - 1, day);
}

function configuration() {
  const base = process.env.RURALZAP_BASE_URL;
  const key = process.env.RURALZAP_API_KEY;
  if (!base || !key) {
    throw new IntegrationError(
      "Configure RURALZAP_BASE_URL e RURALZAP_API_KEY.",
    );
  }
  let origin: URL;
  try {
    origin = new URL(base);
  } catch {
    throw new IntegrationError("RURALZAP_BASE_URL deve ser uma origem válida.");
  }
  const local = ["localhost", "127.0.0.1", "[::1]"].includes(origin.hostname);
  if (
    (origin.protocol !== "https:" && !(local && origin.protocol === "http:")) ||
    origin.username ||
    origin.password ||
    origin.search ||
    origin.hash ||
    origin.pathname !== "/"
  ) {
    throw new IntegrationError(
      "Use uma origem HTTPS sem caminho, credenciais ou query. HTTP só é aceito em localhost.",
    );
  }
  const [startDate, endDate] = process.argv.slice(2);
  if (!startDate || !endDate || process.argv.length !== 4) {
    throw new IntegrationError(
      "Uso: tsx read-transactions.ts YYYY-MM-DD YYYY-MM-DD",
    );
  }
  const days =
    (dateOnlyMilliseconds(endDate) - dateOnlyMilliseconds(startDate)) /
      86_400_000 +
    1;
  if (days < 1 || days > 366) {
    throw new IntegrationError(
      "O período deve ser ordenado e ter até 366 dias inclusivos.",
    );
  }
  return { origin, key, startDate, endDate };
}

function parsePage(value: unknown): TransactionPage {
  if (!isObject(value) || !Array.isArray(value.data) || !isObject(value.meta)) {
    throw new IntegrationError(
      "Resposta incompatível com o contrato de paginação.",
    );
  }
  const { hasMore, nextCursor } = value.meta;
  if (
    typeof hasMore !== "boolean" ||
    (nextCursor !== null && typeof nextCursor !== "string") ||
    (hasMore && (typeof nextCursor !== "string" || nextCursor.length === 0))
  ) {
    throw new IntegrationError("Metadados de paginação inválidos.");
  }
  const rows: unknown[] = value.data;
  const data = rows.map((row): Transaction => {
    if (
      !isObject(row) ||
      typeof row.id !== "string" ||
      row.id.length === 0 ||
      typeof row.amount !== "string" ||
      !MONEY.test(row.amount) ||
      typeof row.currency !== "string" ||
      !/^[A-Z]{3}$/.test(row.currency) ||
      typeof row.date !== "string"
    ) {
      throw new IntegrationError(
        "Lançamento incompatível com o contrato esperado.",
      );
    }
    dateOnlyMilliseconds(row.date);
    // Campos adicionais são ignorados para aceitar evoluções compatíveis da v1.
    return {
      id: row.id,
      amount: row.amount,
      currency: row.currency,
      date: row.date,
    };
  });
  return { data, meta: { hasMore, nextCursor } };
}

function retryDelayMs(response: Response, attempt: number): number {
  const retryAfter = response.headers.get("retry-after");
  const seconds =
    retryAfter !== null && /^\d+$/.test(retryAfter) ? Number(retryAfter) : null;
  const delay =
    seconds === null
      ? response.status === 429
        ? 60_000
        : 1_000 * 2 ** attempt
      : seconds * 1_000;
  if (!Number.isFinite(delay) || delay > 120_000) {
    throw new IntegrationError(
      "Servidor pediu espera longa. Reexecute depois do prazo de Retry-After.",
    );
  }
  return delay + Math.floor(Math.random() * 250);
}

async function fetchPage(url: URL, key: string): Promise<TransactionPage> {
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
    let response: Response;
    try {
      response = await fetch(url, {
        headers: { Authorization: `Bearer ${key}`, Accept: "application/json" },
        signal: AbortSignal.timeout(TIMEOUT_MS),
        redirect: "error", // Nunca encaminhar uma chave por redirecionamento.
      });
    } catch {
      if (attempt === MAX_ATTEMPTS - 1) {
        throw new IntegrationError(
          "Falha de conexão ou timeout após tentativas limitadas.",
        );
      }
      await sleep(1_000 * 2 ** attempt + Math.floor(Math.random() * 250));
      continue;
    }
    if (response.ok) {
      const payload: unknown = await response.json();
      return parsePage(payload);
    }
    const requestId = response.headers.get("x-request-id");
    const safeId =
      requestId && /^[a-f0-9-]{36}$/i.test(requestId)
        ? `; requestId=${requestId}`
        : "";
    // Não ler nem registrar o corpo financeiro ou headers de autenticação.
    await response.body?.cancel();
    if (
      (response.status === 429 || response.status === 503) &&
      attempt < MAX_ATTEMPTS - 1
    ) {
      await sleep(retryDelayMs(response, attempt));
      continue;
    }
    throw new IntegrationError(
      `HTTP ${response.status}${safeId}. Confira o guia de erros; a execução foi encerrada.`,
    );
  }
  throw new IntegrationError("Limite de tentativas atingido.");
}

async function main(): Promise<void> {
  const { origin, key, startDate, endDate } = configuration();
  const url = new URL("/api/v1/integration/transactions", origin);
  url.searchParams.set("startDate", startDate);
  url.searchParams.set("endDate", endDate);
  url.searchParams.set("limit", "100");
  const cursors = new Set<string>();
  let count = 0;
  for (let pageNumber = 1; pageNumber <= MAX_PAGES; pageNumber += 1) {
    const page = await fetchPage(url, key);
    for (const transaction of page.data) {
      // Integração real: faça upsert pelo transaction.id no seu armazenamento.
      // Preserve transaction.amount como string/DECIMAL(…,4), nunca Number/parseFloat.
      // Os campos são validados; este exemplo não grava nem imprime dados financeiros.
      void transaction;
      count += 1;
    }
    if (!page.meta.hasMore) {
      console.log(
        `Consulta concluída: ${count} lançamentos recebidos em ${pageNumber} página(s).`,
      );
      return;
    }
    const cursor = page.meta.nextCursor;
    if (!cursor || cursors.has(cursor)) {
      throw new IntegrationError(
        "Cursor ausente ou repetido; consulta interrompida.",
      );
    }
    cursors.add(cursor);
    url.searchParams.set("cursor", cursor);
  }
  throw new IntegrationError(
    "Limite local de páginas atingido. Divida a consulta em períodos menores.",
  );
}

// Releia o período para conciliar alterações/exclusões por id: não é feed incremental
// nem snapshot. Não derive receita ou saldo somando lançamentos; use os relatórios.
void main().catch((error: unknown) => {
  console.error(
    error instanceof IntegrationError
      ? error.message
      : "Falha local ao processar a resposta. Nenhum payload foi registrado.",
  );
  process.exitCode = 1;
});

```

## Leitor completo em Python [#leitor-completo-em-python]

[Baixar read-transactions.py](/examples/read-transactions.py)

```python
"""RuralZap: Python 3.10+, somente biblioteca padrão, execução no servidor.

python read-transactions.py 2026-01-01 2026-01-31
Configure RURALZAP_BASE_URL e RURALZAP_API_KEY no ambiente privado.
O exemplo valida dados e imprime somente a contagem; não persiste lançamentos.
"""

import json
import os
import random
import re
import sys
import time
from datetime import date
from decimal import Decimal
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode, urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener

MAX_ATTEMPTS = 4
MAX_PAGES = 10_000
TIMEOUT_SECONDS = 30
MONEY = re.compile(r"^-?\d+\.\d{4}$")


class IntegrationError(Exception):
    """Mensagens controladas, sem credenciais ou respostas financeiras."""


class NoRedirects(HTTPRedirectHandler):
    def redirect_request(self, request, response, code, message, headers, new_url):
        # Não encaminhar a chave para uma origem indicada por redirecionamento.
        return None


def configuration():
    origin = os.environ.get("RURALZAP_BASE_URL", "")
    key = os.environ.get("RURALZAP_API_KEY", "")
    if not origin or not key:
        raise IntegrationError("Configure RURALZAP_BASE_URL e RURALZAP_API_KEY.")
    try:
        parsed = urlsplit(origin)
        local = parsed.hostname in {"localhost", "127.0.0.1", "::1"}
        valid_transport = parsed.scheme == "https" or (local and parsed.scheme == "http")
        valid_origin = parsed.hostname and not (
            parsed.username or parsed.password or parsed.query or parsed.fragment
            or parsed.path not in {"", "/"}
        )
    except ValueError:
        raise IntegrationError("RURALZAP_BASE_URL deve ser uma origem válida.") from None
    if not valid_transport or not valid_origin:
        raise IntegrationError("Use uma origem HTTPS sem caminho, credenciais ou query. HTTP só é aceito em localhost.")
    if len(sys.argv) != 3:
        raise IntegrationError("Uso: python read-transactions.py YYYY-MM-DD YYYY-MM-DD")
    start_date, end_date = sys.argv[1:]
    try:
        if not all(re.fullmatch(r"\d{4}-\d{2}-\d{2}", value) for value in (start_date, end_date)):
            raise ValueError()
        days = (date.fromisoformat(end_date) - date.fromisoformat(start_date)).days + 1
    except ValueError:
        raise IntegrationError("Use datas válidas no formato YYYY-MM-DD.") from None
    if not 1 <= days <= 366:
        raise IntegrationError("O período deve ser ordenado e ter até 366 dias inclusivos.")
    return origin.rstrip("/"), key, start_date, end_date


def parse_page(value):
    if not isinstance(value, dict) or not isinstance(value.get("data"), list) or not isinstance(value.get("meta"), dict):
        raise IntegrationError("Resposta incompatível com o contrato de paginação.")
    has_more = value["meta"].get("hasMore")
    next_cursor = value["meta"].get("nextCursor")
    if type(has_more) is not bool or (next_cursor is not None and not isinstance(next_cursor, str)) or (has_more and not next_cursor):
        raise IntegrationError("Metadados de paginação inválidos.")
    rows = []
    for row in value["data"]:
        if not isinstance(row, dict):
            raise IntegrationError("Lançamento inválido.")
        identifier, amount, currency, financial_date = (row.get(field) for field in ("id", "amount", "currency", "date"))
        if not (
            isinstance(identifier, str) and identifier
            and isinstance(amount, str) and MONEY.fullmatch(amount)
            and isinstance(currency, str) and re.fullmatch(r"[A-Z]{3}", currency)
            and isinstance(financial_date, str) and re.fullmatch(r"\d{4}-\d{2}-\d{2}", financial_date)
        ):
            raise IntegrationError("Lançamento incompatível com o contrato esperado.")
        try:
            date.fromisoformat(financial_date)
        except ValueError:
            raise IntegrationError("Data financeira inválida na resposta.") from None
        # Decimal recebe a string original: nunca passar por float.
        rows.append({"id": identifier, "amount": Decimal(amount), "currency": currency, "date": financial_date})
    return rows, has_more, next_cursor


def retry_delay(error, attempt):
    raw = error.headers.get("Retry-After")
    if raw is not None and re.fullmatch(r"\d+", raw):
        delay = int(raw)
    else:
        delay = 60 if error.code == 429 else 2 ** attempt
    if delay > 120:
        raise IntegrationError("Servidor pediu espera longa. Reexecute depois do prazo de Retry-After.")
    return delay + random.uniform(0, 0.25)


def fetch_page(opener, url, key):
    for attempt in range(MAX_ATTEMPTS):
        request = Request(url, headers={"Authorization": f"Bearer {key}", "Accept": "application/json"}, method="GET")
        try:
            with opener.open(request, timeout=TIMEOUT_SECONDS) as response:
                return parse_page(json.load(response))
        except HTTPError as error:
            # Nunca ler ou registrar body, URL ou headers que contenham credenciais.
            request_id = error.headers.get("X-Request-Id", "")
            safe_id = f"; requestId={request_id}" if re.fullmatch(r"[a-fA-F0-9-]{36}", request_id) else ""
            error.close()
            if error.code in {429, 503} and attempt < MAX_ATTEMPTS - 1:
                time.sleep(retry_delay(error, attempt))
                continue
            raise IntegrationError(f"HTTP {error.code}{safe_id}. Confira o guia de erros; a execução foi encerrada.") from None
        except (URLError, TimeoutError):
            if attempt == MAX_ATTEMPTS - 1:
                raise IntegrationError("Falha de conexão ou timeout após tentativas limitadas.") from None
            time.sleep(2 ** attempt + random.uniform(0, 0.25))
    raise IntegrationError("Limite de tentativas atingido.")


def main():
    origin, key, start_date, end_date = configuration()
    parameters = {"startDate": start_date, "endDate": end_date, "limit": "100"}
    opener = build_opener(NoRedirects())
    cursors = set()
    count = 0
    for page_number in range(1, MAX_PAGES + 1):
        url = f"{origin}/api/v1/integration/transactions?{urlencode(parameters)}"
        rows, has_more, next_cursor = fetch_page(opener, url, key)
        for transaction in rows:
            # Integração real: upsert pelo transaction["id"] no seu armazenamento.
            # Para salvar amount: coluna DECIMAL(…,4) ou format(transaction["amount"], ".4f").
            # Este exemplo não grava nem imprime dados financeiros.
            count += 1
        if not has_more:
            print(f"Consulta concluída: {count} lançamentos recebidos em {page_number} página(s).")
            return
        if not next_cursor or next_cursor in cursors:
            raise IntegrationError("Cursor ausente ou repetido; consulta interrompida.")
        cursors.add(next_cursor)
        parameters["cursor"] = next_cursor
    raise IntegrationError("Limite local de páginas atingido. Divida a consulta em períodos menores.")


if __name__ == "__main__":
    try:
        # Releia o período para conciliar alterações/exclusões por id; não é feed
        # incremental nem snapshot. Para receitas/saldos, use os relatórios canônicos.
        main()
    except IntegrationError as error:
        print(str(error), file=sys.stderr)
        sys.exit(1)
    except Exception:
        print("Falha local ao processar a resposta. Nenhum payload foi registrado.", file=sys.stderr)
        sys.exit(1)

```
