Backend API reference
Base URL is whatever PORT streamgive-backend
is running on (http://localhost:3000 locally). All responses are JSON.
Every amount field (rate, balance, withdrawn, totalCommitted,
totalWithdrawn) is a decimal string, not a number — these are i128
values on-chain and can exceed what a JS/JSON number can represent
exactly. onChainId is likewise a string, not a raw BigInt.
Public, read-only
GET /health
Pings the database. { "status": "ok" } on success.
GET /ngos
Verified NGOs, newest first, capped at 100.
[{ "id": "...", "ownerAddress": "G...", "name": "...", "verified": true, "createdAt": "...", "updatedAt": "..." }]
GET /ngos/:id
One NGO's profile plus stats computed from its streams. id is the
internal id from GET /ngos, not a Stellar address. 400 if id isn't a
UUID, 404 if it doesn't exist.
{
"id": "...", "ownerAddress": "G...", "name": "...", "verified": true,
"stats": {
"totalCommitted": "1000000000",
"totalWithdrawn": "500000000",
"activeStreamCount": 3,
"donorCount": 2
}
}
totalCommitted is balance + withdrawn summed across all of the NGO's
streams — not the same as the original deposits once top-ups or
cancellations have happened. See the architecture
page for why this can lag on-chain confirmations by a few seconds.
GET /impact/:ngoId
A superset of /ngos/:id's stats, framed around relative impact: active
vs. cancelled stream counts, and this NGO's share of everything ever
committed platform-wide. 404 if the NGO doesn't exist.
{
"ngoId": "...", "name": "...",
"totalCommitted": "1000000000", "totalWithdrawn": "500000000",
"activeStreams": 2, "cancelledStreams": 1,
"uniqueDonors": 2, "platformSharePercent": 12.5
}
GET /streams?donor=&ngo=
Both filters optional (an empty query returns everything, capped at 100).
donor is a Stellar address (^G[A-Z2-7]{55}$); ngo is the NGO's
internal id. 400 if either fails validation.
[{
"id": "...", "onChainId": "1", "tokenAddress": "C...",
"rate": "10", "balance": "990", "withdrawn": "10",
"status": "ACTIVE", "createdAt": "...", "updatedAt": "...",
"donor": { "address": "G..." },
"ngo": { "id": "...", "name": "...", "ownerAddress": "G..." }
}]
Public, write
POST /ngo-applications
Off-chain intake — reviewed by an admin before anyone calls the on-chain
approve_ngo. Rate-limited to 5 requests/minute (tighter than the
platform-wide 100/minute default, since it's the most spam-prone route in
the API).
// request body
{
"ownerAddress": "G...",
"name": "...",
"description": "...",
"contactEmail": "you@example.org",
"website": "https://example.org", // optional
"country": "..." // optional
}
201 with the created application (status: "PENDING") on success,
400 on validation failure, 409 if that address already has a pending
application.
Admin-only
These three require a signed request — see Authentication
below. 503 if the backend has no ADMIN_ADDRESS configured; 401 if
the signature is missing, invalid, or stale.
GET /ngo-applications?status=
status optional (PENDING | APPROVED | REJECTED); omitted returns
everything, capped at 100.
POST /ngo-applications/:id/approve
POST /ngo-applications/:id/reject
Both accept an optional body { "reviewNote": "..." } and return the
updated application. Neither of these calls the on-chain approve_ngo —
that's a separate, deliberate step the frontend's platform admin panel
does first (see architecture); this endpoint only
records the review decision.
Authentication
Admin routes are gated by SEP-53 message signing, not a session or API key. To call one:
- Build
payload = "${method}:${path}:${timestamp}"—pathis exactly what the server sees as the request path and query string (e.g./ngo-applications?status=PENDING), with no scheme or host, andtimestampis the current time in epoch milliseconds. - Sign
payloadwith your wallet's generic message-signing call (not transaction signing) — the wallet applies the SEP-53 prefix and SHA-256 hash itself. - Send three headers:
x-admin-address(your public key),x-admin-signature(the signature, base64),x-admin-timestamp(the same timestamp used above, as a string).
The server rejects the request if x-admin-address doesn't match the
configured ADMIN_ADDRESS, if the timestamp is more than 5 minutes old
(bounding replay of a captured header set), or if the signature doesn't
verify. See streamgive-frontend's src/lib/adminApi.ts for a complete,
working implementation of this flow.