Transo · Wallet SMS hub
Documentation
Transo turns mobile money and payment SMS from across the Horn of Africa into a live business dashboard. Pair your shop phones, sync wallet messages securely, and run reports without manual entry.
Transo · Wallet SMS hub
Transo turns mobile money and payment SMS from across the Horn of Africa into a live business dashboard. Pair your shop phones, sync wallet messages securely, and run reports without manual entry.
localhost) so phones can reach the server.
The dashboard shows today's money in and out, activity insights, and a searchable payment table.
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.
Read the full privacy & data isolation policy.
| Role | Access |
|---|---|
| Owner | Full access — devices, marketing, billing, settings |
| Accountant | Dashboard & reports only |
| Viewer | Read-only dashboard & reports |
Owners can create a time-limited read-only link (Settings → Share with accountant). Accountants see aggregates and can download CSV — they cannot change data.
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.
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_….
https://transo.cloud (production).
Self-hosted installs use your own domain or LAN IP instead.
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");
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 };
}
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());
}
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 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());
});
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
$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]);
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)
}
| Method | Path | Description |
|---|---|---|
GET | https://transo.cloud/api/v1/developer/transactions | limit, fromMs, toMs, provider |
GET | https://transo.cloud/api/v1/developer/stats | fromMs (default last 30 days) |
GET | https://transo.cloud/api/v1/developer/devices | Paired phones for your account |
GET | https://transo.cloud/api/v1/developer/account | Plan and limits |
POST | https://transo.cloud/api/v1/developer/mcp | MCP JSON-RPC for AI tools |
Plans control device limits, API quotas, marketing sends, and history retention. Pay via Stripe or submit a mobile money transfer reference for manual confirmation.
By design, synced transactions are wiped when the last web session ends. Devices stay paired and re-sync automatically.
No. The Android app parses SMS locally and sends structured payment rows over TLS.
Yes — use CSV export or the accountant share link CSV endpoint.