Sign in Begin

Getting started

  1. Create an account at Sign up.
  2. Open Devices in the dashboard and generate a QR pairing code.
  3. Scan with the Transo Android app on each shop phone.
  4. Watch payments appear on the dashboard as SMS syncs in real time.
Tip Production dashboard and pairing API live at https://transo.cloud. Self-hosted installs should use a LAN IP (not localhost) so phones can reach the server.

Dashboard

The dashboard shows today's money in and out, activity insights, and a searchable payment table.

  • Multi-wallet tabs — filter by each paired phone when you run several shop lines.
  • Report manager — export CSV, Excel, or PDF for any date range.
  • Offline mode — the shell loads from cache when the network is slow; data refreshes when back online.

Devices & pairing

Each paired phone gets a unique device token. Device metadata and sync state are stored in your isolated user database (data/users/u<id>.db), not in the browser.

Dashboard → Devices → Show QR → Scan in Android app

Revoking a device removes its transactions from your user database. Logging out wipes synced rows when it is the last active session — phones re-upload on next connect.

Privacy & security

  • Transactions live in a separate SQLite file per account.
  • Platform admins cannot query individual wallet rows.
  • Sessions use httpOnly cookies; device tokens are stored hashed server-side.
  • Optional TOTP two-factor authentication for owners.

Read the full privacy & data isolation policy.

Team & roles

RoleAccess
OwnerFull access — devices, marketing, billing, settings
AccountantDashboard & reports only
ViewerRead-only dashboard & reports

Accountant sharing

Owners can create a time-limited read-only link (Settings → Share with accountant). Accountants see aggregates and can download CSV — they cannot change data.

Developer API

Authenticate with Authorization: Bearer transo_live_…

GET  https://transo.cloud/api/v1/developer/transactions
GET  https://transo.cloud/api/v1/developer/stats
GET  https://transo.cloud/api/v1/developer/devices
POST https://transo.cloud/api/v1/developer/mcp

Manage keys and webhooks in the Developers panel. Usage is metered per plan.

SDK integrations

All examples use your API key from Developers. Replace YOUR_API_KEY with your secret. API base URL: https://transo.cloud. Every request must send Authorization: Bearer transo_live_….

Base URLhttps://transo.cloud (production). Self-hosted installs use your own domain or LAN IP instead.

JavaScript (Node & browser)

Works in Node 18+, Deno, Bun, and modern browsers with fetch.

const BASE = process.env.TRANSO_API_URL ?? "https://transo.cloud";
const KEY = process.env.TRANSO_API_KEY;

async function transo(path, params = {}) {
  const url = new URL(`/api/v1/developer${path}`, BASE);
  Object.entries(params).forEach(([k, v]) => {
    if (v != null && v !== "") url.searchParams.set(k, String(v));
  });
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${KEY}` },
  });
  if (!res.ok) throw new Error(await res.text());
  return res.json();
}

// Usage
const { transactions } = await transo("/transactions", { limit: 50 });
const { stats } = await transo("/stats", { fromMs: Date.now() - 7 * 864e5 });
const { devices } = await transo("/devices");

React

Fetch inside a hook; keep the API key in a server-side env var or a secure backend proxy — never ship live keys in client bundles.

import { useEffect, useState } from "react";

const API = import.meta.env.VITE_TRANSO_API_URL ?? "https://transo.cloud";
const KEY = import.meta.env.VITE_TRANSO_API_KEY; // dev only — proxy in production

export function useTransoStats(fromMs) {
  const [stats, setStats] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const res = await fetch(
          `${API}/api/v1/developer/stats?fromMs=${fromMs}`,
          { headers: { Authorization: `Bearer ${KEY}` } }
        );
        if (!res.ok) throw new Error("transo_api_error");
        const data = await res.json();
        if (!cancelled) setStats(data.stats);
      } catch (e) {
        if (!cancelled) setError(e);
      }
    })();
    return () => { cancelled = true; };
  }, [fromMs]);

  return { stats, error };
}

Next.js (App Router)

Call Transo from a Route Handler or Server Component so the key stays on the server.

// app/api/wallet/stats/route.ts
import { NextResponse } from "next/server";

export async function GET() {
  const base = process.env.TRANSO_API_URL ?? "https://transo.cloud";
  const key = process.env.TRANSO_API_KEY!;
  const fromMs = Date.now() - 30 * 86400000;

  const res = await fetch(`${base}/api/v1/developer/stats?fromMs=${fromMs}`, {
    headers: { Authorization: `Bearer ${key}` },
    next: { revalidate: 60 },
  });

  if (!res.ok) {
    return NextResponse.json({ error: "upstream_failed" }, { status: 502 });
  }
  return NextResponse.json(await res.json());
}

Vue / Nuxt

Use a composable (Vue 3) or server route (Nuxt) with the same fetch pattern as React.

// composables/useTranso.ts — Nuxt: useRuntimeConfig() for URL + key on server
export async function fetchTranso<T>(path: string, query: Record<string, string> = {}) {
  const config = useRuntimeConfig();
  const url = new URL(`/api/v1/developer${path}`, config.transoApiUrl);
  Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v));

  return $fetch<T>(url.toString(), {
    headers: { Authorization: `Bearer ${config.transoApiKey}` },
  });
}

// const { stats } = await fetchTranso("/stats", { fromMs: String(Date.now() - 864e5) });

Express / Fastify (Node backend)

// Express proxy — keeps the API key off the frontend
import express from "express";

const app = express();
const BASE = process.env.TRANSO_API_URL ?? "https://transo.cloud";
const KEY = process.env.TRANSO_API_KEY;

app.get("/api/my-wallet/transactions", async (req, res) => {
  const limit = req.query.limit ?? "100";
  const upstream = await fetch(
    `${BASE}/api/v1/developer/transactions?limit=${limit}`,
    { headers: { Authorization: `Bearer ${KEY}` } }
  );
  res.status(upstream.status).json(await upstream.json());
});

Python

import os
import requests

BASE = os.environ.get("TRANSO_API_URL", "https://transo.cloud").rstrip("/")
KEY = os.environ["TRANSO_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}"}

def transo_get(path: str, **params):
    r = requests.get(f"{BASE}/api/v1/developer{path}", headers=HEADERS, params=params, timeout=30)
    r.raise_for_status()
    return r.json()

if __name__ == "__main__":
    tx = transo_get("/transactions", limit=20)
    stats = transo_get("/stats", fromMs=int(__import__("time").time() * 1000) - 7 * 86400000)
    devices = transo_get("/devices")
    print(len(tx["transactions"]), stats["stats"], len(devices["devices"]))

PHP

<?php
$base = rtrim(getenv('TRANSO_API_URL') ?: 'https://transo.cloud', '/');
$key = getenv('TRANSO_API_KEY');

function transo_get(string $path, array $query = []): array {
    global $base, $key;
    $url = $base . '/api/v1/developer' . $path;
    if ($query) {
        $url .= '?' . http_build_query($query);
    }
    $ctx = stream_context_create([
        'http' => [
            'header' => "Authorization: Bearer {$key}\r\nAccept: application/json\r\n",
            'timeout' => 30,
        ],
    ]);
    $body = file_get_contents($url, false, $ctx);
    if ($body === false) {
        throw new RuntimeException('Transo request failed');
    }
    return json_decode($body, true, flags: JSON_THROW_ON_ERROR);
}

$transactions = transo_get('/transactions', ['limit' => 50]);
$stats = transo_get('/stats', ['fromMs' => (time() - 86400 * 30) * 1000]);

Go

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"time"
)

func transoGet(path string, query url.Values) (map[string]any, error) {
	base := os.Getenv("TRANSO_API_URL")
	if base == "" {
		base = "https://transo.cloud"
	}
	key := os.Getenv("TRANSO_API_KEY")
	u, _ := url.Parse(base + "/api/v1/developer" + path)
	u.RawQuery = query.Encode()

	req, _ := http.NewRequest(http.MethodGet, u.String(), nil)
	req.Header.Set("Authorization", "Bearer "+key)

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)
	if res.StatusCode >= 400 {
		return nil, fmt.Errorf("transo %s: %s", res.Status, body)
	}
	var out map[string]any
	return out, json.Unmarshal(body, &out)
}

func main() {
	from := fmt.Sprintf("%d", time.Now().Add(-30*24*time.Hour).UnixMilli())
	data, _ := transoGet("/stats", url.Values{"fromMs": {from}})
	fmt.Println(data)
}

Common endpoints

MethodPathDescription
GEThttps://transo.cloud/api/v1/developer/transactionslimit, fromMs, toMs, provider
GEThttps://transo.cloud/api/v1/developer/statsfromMs (default last 30 days)
GEThttps://transo.cloud/api/v1/developer/devicesPaired phones for your account
GEThttps://transo.cloud/api/v1/developer/accountPlan and limits
POSThttps://transo.cloud/api/v1/developer/mcpMCP JSON-RPC for AI tools

Billing & plans

Plans control device limits, API quotas, marketing sends, and history retention. Pay via Stripe or submit a mobile money transfer reference for manual confirmation.

FAQ

Why did my dashboard empty after logout?

By design, synced transactions are wiped when the last web session ends. Devices stay paired and re-sync automatically.

Does Transo read my SMS on the server?

No. The Android app parses SMS locally and sends structured payment rows over TLS.

Can I export for QuickBooks?

Yes — use CSV export or the accountant share link CSV endpoint.