API Documentation

BI Integration Recipes

Copy-paste examples for connecting common BI tools and runtimes to the Saldo.tech Reporting API. Each recipe assumes you've generated an institution API key — if not, head to Dashboard → API Keys to create one.

See the full endpoint reference and authentication details in the API documentation.

cURL

Quickest way to verify your key works and explore response shapes from the terminal.

# Last 30 days of redemptions
curl https://api.saldo.tech/api/v1/reporting/redemptions \
  -H "Authorization: Bearer slk_..." \
  | jq '.data | length'

# Filter by date range
curl "https://api.saldo.tech/api/v1/reporting/redemptions?from=2026-04-01T00:00:00Z&to=2026-05-01T00:00:00Z" \
  -H "Authorization: Bearer slk_..."

# Engagement funnel for the last 7 days
FROM=$(date -u -v-7d +"%Y-%m-%dT%H:%M:%SZ")
curl "https://api.saldo.tech/api/v1/reporting/funnel?from=$FROM" \
  -H "Authorization: Bearer slk_..."

Python (requests + pandas)

Pull all redemptions into a DataFrame for ad-hoc analysis or pipeline ingestion. Handles pagination automatically.

import os
import requests
import pandas as pd

API_KEY = os.environ["SALDO_API_KEY"]
BASE = "https://api.saldo.tech/api/v1/reporting"

def fetch_all(path: str, params: dict | None = None) -> list[dict]:
    rows: list[dict] = []
    cursor = None
    while True:
        q = {"limit": 1000, **(params or {})}
        if cursor:
            q["cursor"] = cursor
        r = requests.get(
            f"{BASE}/{path}",
            headers={"Authorization": f"Bearer {API_KEY}"},
            params=q,
            timeout=30,
        )
        r.raise_for_status()
        body = r.json()
        rows.extend(body["data"])
        cursor = body["pagination"]["next_cursor"]
        if not cursor:
            return rows

# Last quarter's redemptions
redemptions = fetch_all(
    "redemptions",
    {"from": "2026-02-01T00:00:00Z", "to": "2026-05-01T00:00:00Z"},
)
df = pd.json_normalize(redemptions)
print(df.groupby("offer.merchant_name")["institution_payout_amount"].sum().sort_values(ascending=False))

Node.js (fetch)

Drop-in async iterator for paging through large result sets without buffering everything in memory.

const API_KEY = process.env.SALDO_API_KEY;
const BASE = "https://api.saldo.tech/api/v1/reporting";

async function* paginate(path, params = {}) {
  let cursor = null;
  while (true) {
    const url = new URL(`${BASE}/${path}`);
    Object.entries({ limit: 1000, ...params }).forEach(([k, v]) =>
      url.searchParams.set(k, String(v)),
    );
    if (cursor) url.searchParams.set("cursor", cursor);

    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${API_KEY}` },
    });
    if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
    const body = await res.json();
    for (const row of body.data) yield row;
    cursor = body.pagination.next_cursor;
    if (!cursor) return;
  }
}

let total = 0;
for await (const settlement of paginate("settlements", { status: "settled" })) {
  total += settlement.institution_payout_amount;
}
console.log(`Total settled payouts: $${total.toFixed(2)}`);

Google Sheets (Apps Script)

Refresh a sheet with the latest funnel KPIs on demand. Open Extensions → Apps Script and paste this in.

function refreshFunnel() {
  const apiKey = PropertiesService.getScriptProperties().getProperty("SALDO_API_KEY");
  const url = "https://api.saldo.tech/api/v1/reporting/funnel";
  const res = UrlFetchApp.fetch(url, {
    headers: { Authorization: "Bearer " + apiKey },
    muteHttpExceptions: true,
  });
  const body = JSON.parse(res.getContentText());

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Funnel");
  sheet.getRange("A1").setValue("Last refreshed");
  sheet.getRange("B1").setValue(new Date());
  sheet.getRange("A3:B3").setValues([["Metric", "Value"]]);
  sheet.getRange("A4:B8").setValues([
    ["Widget Views", body.data.widget_views],
    ["Unique Cardholders", body.data.unique_cardholders],
    ["Offer Impressions", body.data.offer_impressions],
    ["Offer Adds", body.data.offer_add_successes],
    ["Redemptions", body.data.redemptions],
  ]);
}

// Set up: Project Settings → Script Properties → add SALDO_API_KEY
// Then Triggers → Add Trigger → refreshFunnel → Time-driven → daily.

Power BI (Power Query M)

Connect Power BI directly to your settlement ledger as a refreshable data source. Loops through the cursor automatically.

let
    ApiKey = "slk_...",
    BaseUrl = "https://api.saldo.tech/api/v1/reporting/settlements",

    FetchPage = (cursor as nullable text) =>
        let
            Query = if cursor = null then [limit="1000"] else [limit="1000", cursor=cursor],
            Source = Json.Document(Web.Contents(BaseUrl, [
                Query = Query,
                Headers = [#"Authorization" = "Bearer " & ApiKey]
            ]))
        in Source,

    Loop = List.Generate(
        () => FetchPage(null),
        each Record.HasFields(_, "data") and List.Count(_[data]) > 0,
        each FetchPage(_[pagination][next_cursor]),
        each _[data]
    ),

    AllRows = List.Combine(Loop),
    AsTable = Table.FromRecords(AllRows),
    Typed = Table.TransformColumnTypes(AsTable, {
        {"created_at", type datetimezone},
        {"settled_at", type datetimezone},
        {"merchant_funded_amount", Currency.Type},
        {"saldo_revenue", Currency.Type},
        {"institution_payout_amount", Currency.Type}
    })
in
    Typed

Metabase (HTTP question)

Drop-in Native HTTP question for Metabase's HTTP request driver. Works for any of the JSON reporting endpoints.

{
  "method": "GET",
  "url": "https://api.saldo.tech/api/v1/reporting/redemptions",
  "headers": {
    "Authorization": "Bearer {{secret_api_key}}"
  },
  "parameters": {
    "from": "{{from_date}}",
    "to": "{{to_date}}",
    "limit": 1000
  },
  "result": {
    "path": "data"
  }
}

# Define a Metabase secret named secret_api_key holding your slk_... value.
# Pass from_date / to_date as Metabase parameters in the dashboard filter UI.
# Note: cursor pagination is single-page in this driver — for more than
# 1000 rows, use a scheduled ETL into a SQL warehouse Metabase can read.

Need a recipe we don't have?

Tell us what BI stack you're using — we're prioritizing the most-requested integrations next.