Developer infrastructure for the Stellar ecosystem.
| Environment | URL | Notes |
|---|---|---|
| Mainnet | https://api.savitools.com/api |
Production Stellar network |
| Testnet | https://testnet-api.savitools.com/api |
Stellar testnet environment |
| Local Development | http://localhost:3001/api |
Development server (default) |
The API uses URI-based versioning. All endpoints are prefixed with /v1 (or the version number). The current default version is v1.
Example: GET /api/v1/health
- Public endpoints: No authentication required (e.g.,
/wallet/generate,/simulator/paths) - Protected endpoints: Require valid JWT authentication via HTTP-only cookies
- Register or login to create a session
- Use the issued JWT cookie for subsequent requests
The API uses HTTP-only cookies to store JWT tokens automatically after authentication. When you call POST /auth/login or POST /auth/register, the response sets:
access_tokencookie (15-minute expiration)refresh_tokencookie (7-day expiration)
All subsequent requests automatically include these cookies. No header configuration needed.
If cookies are disabled, use:
Authorization: Bearer {accessToken}
To refresh an expired access token:
curl -X POST http://localhost:3001/api/v1/auth/refresh \
-H "Content-Type: application/json" \
--cookie "refresh_token=YOUR_REFRESH_TOKEN"These public, read-only endpoints inspect the domain's /.well-known/stellar.toml. They do not store results or require a user session. TOML responses use the existing five-minute, bounded in-memory cache (up to 200 domains); concurrent requests for the same domain share a fetch. Fetches retain the federation module's timeout, response-size, redirect, and public-host SSRF limits. No secrets are accepted or returned.
Checks the issuer key appears in the domain's ACCOUNTS array. An optional account HOME_DOMAIN value must also match the normalized domain. A mismatch is returned as a successful validation result with valid: false; malformed inputs use the standard 400 error envelope and an unavailable TOML uses the existing federation error responses.
Response (200):
{ "valid": true, "domain": "example.com", "issuer": "G...", "reason": null }Returns the matching [[CURRENCIES]] metadata only when the issuer passes the home-domain check. Asset codes must contain 1–12 ASCII letters or digits and issuer must be a Stellar public key. An undeclared currency returns 404; an issuer that fails domain validation returns 400.
Response (200):
{ "code": "USDC", "issuer": "G...", "name": "USD Coin", "display_decimals": 7 }The existing FEDERATION_TOML_CACHE_TTL_MS and FEDERATION_TOML_CACHE_MAX_ENTRIES settings control cache behavior (defaults: 5 minutes and 200 domains). TOML fetches have a 15-second timeout. FEDERATION_REQUEST_TIMEOUT_MS (default 5 seconds) is the overall SEP inspection deadline; FEDERATION_PROBE_TIMEOUT_MS (default 3 seconds) bounds each endpoint probe. Invalid or non-positive setting values use their defaults. TOML payloads are limited to 512 KiB, nesting depth 64, and 10,000 parsed keys.
Health check endpoint.
Request:
curl http://localhost:3001/api/v1/healthResponse (200):
{
"status": "ok"
}Register a new user with email and password.
Request:
curl -X POST http://localhost:3001/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "SecurePassword123"
}'Response (201):
{
"user": {
"id": "user-uuid",
"email": "user@example.com",
"fluxaTenantId": null
}
}Errors:
400: User already exists or invalid email format
Login with email and password.
Request:
curl -X POST http://localhost:3001/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "SecurePassword123"
}'Response (200):
{
"user": {
"id": "user-uuid",
"email": "user@example.com",
"fluxaTenantId": null
}
}Cookies Set:
access_token(15 min TTL)refresh_token(7 day TTL)
Errors:
401: Invalid email or password
Request a password reset email (see #196). The response is identical whether or not the account exists — the endpoint cannot be used to enumerate registered emails. Requests are rate-limited per IP and per email.
Request:
curl -X POST http://localhost:3001/api/v1/auth/forgot-password \
-H "Content-Type: application/json" \
-d '{ "email": "user@example.com" }'Response (200):
{
"message": "If an account with that email exists, we have sent a link to reset your password."
}The email contains a link to /reset-password?token=…. The token is stored hashed (SHA-256), is single-use, and expires after 30 minutes.
Set a new password with a valid, unused, unexpired reset token. On success every active refresh-token family for the user is revoked, signing out all other sessions.
Request:
curl -X POST http://localhost:3001/api/v1/auth/reset-password \
-H "Content-Type: application/json" \
-d '{
"token": "RESET_TOKEN_FROM_EMAIL",
"password": "NewSecurePassword123"
}'Response (200):
{
"message": "Password updated. You can now sign in with your new password."
}Errors:
404: Reset token is invalid (or already used)410:RESET_TOKEN_EXPIRED
Rotate refresh token and issue a new access token.
Request:
curl -X POST http://localhost:3001/api/v1/auth/refresh \
--cookie "refresh_token=YOUR_REFRESH_TOKEN"Response (200):
{
"user": {
"id": "user-uuid",
"email": "user@example.com"
}
}Errors:
401: Invalid or expired refresh token
Invalidate refresh token and clear auth cookies.
Request:
curl -X POST http://localhost:3001/api/v1/auth/logoutResponse (200):
{
"success": true
}Exchange a Fluxa API key for a SaviTools session and link accounts.
Request:
curl -X POST http://localhost:3001/api/v1/auth/fluxa \
-H "Content-Type: application/json" \
-d '{
"fluxaApiKey": "your-fluxa-api-key"
}'Response (200):
{
"user": {
"id": "user-uuid",
"email": "user@example.com",
"fluxaTenantId": "fluxa-tenant-id"
}
}Errors:
400: Invalid Fluxa API key
Get the current authenticated user.
Request:
curl http://localhost:3001/api/v1/auth/me \
--cookie "access_token=YOUR_ACCESS_TOKEN"Response (200):
{
"user": {
"id": "user-uuid",
"email": "user@example.com",
"fluxaTenantId": null
}
}Errors:
401: Not authenticated
Generate a new Stellar keypair (public key + secret).
Request:
curl -X POST http://localhost:3001/api/v1/wallet/generateResponse (201):
{
"publicKey": "GBZR7WLLV5OZVUQ4WAWCKVCOVWGZFZVHG5GMRFYVZJZ2AFSGHFKDQ4C",
"secret": "SBUQ54DRQG5Q3QLQHJEZ5ODSLGE...TRUNCATED"
}Fund a testnet account via Friendbot (10 XLM).
Request:
curl -X POST http://localhost:3001/api/v1/wallet/fund \
-H "Content-Type: application/json" \
-d '{
"publicKey": "GBZR7WLLV5OZVUQ4WAWCKVCOVWGZFZVHG5GMRFYVZJZ2AFSGHFKDQ4C"
}'Response (200):
{
"success": true,
"amount": "10.0000000",
"currency": "XLM",
"transactionHash": "6c1e1f6..."
}Errors:
400: Invalid public key or funding failed (rate-limited, etc.)
Get asset balances for a Stellar account.
Request:
curl "http://localhost:3001/api/v1/wallet/balances?publicKey=GBZR7WLLV5OZVUQ4WAWCKVCOVWGZFZVHG5GMRFYVZJZ2AFSGHFKDQ4C"Response (200):
{
"balances": [
{
"asset_type": "native",
"balance": "9.9999800",
"asset_code": "XLM"
},
{
"asset_type": "credit_alphanum4",
"asset_code": "USDC",
"asset_issuer": "GA...",
"balance": "100.0000000",
"limit": "922337203685.4775807"
}
]
}Errors:
400: Invalid public key or account not found
Send a payment from a sandbox wallet (requires JWT authentication and rate limiting).
Request:
curl -X POST http://localhost:3001/api/v1/wallet/payment \
-H "Content-Type: application/json" \
-d '{
"sourceSecret": "SBUQ54DRQG5Q3QLQHJEZ5ODSLGE...",
"destination": "GBZR7WLLV5OZVUQ4WAWCKVCOVWGZFZVHG5GMRFYVZJZ2AFSGHFKDQ4C",
"asset": "XLM",
"amount": "5.00"
}'Response (200):
{
"transactionHash": "6c1e1f6fe...",
"success": true,
"amount": "5.0000000",
"destination": "GBZR7..."
}Errors:
400: Invalid parameters or insufficient balance
GET /simulator/paths?direction=...&source_asset_*=...&destination_asset_*=...&amount=...&network=...
Find payment paths between two assets.
Query Parameters:
direction(required):strict_sendorstrict_receivesource_asset_type(required):native|credit_alphanum4|credit_alphanum12source_asset_code(optional): Asset code (e.g.,USDC)source_asset_issuer(optional): Asset issuer public keydestination_asset_type(required): Asset type for destinationdestination_asset_code(optional): Destination asset codedestination_asset_issuer(optional): Destination asset issueramount(required): Amount to send/receivenetwork(optional, defaultmainnet):mainnetortestnet
Request:
curl "http://localhost:3001/api/v1/simulator/paths?direction=strict_send&source_asset_type=native&destination_asset_type=credit_alphanum4&destination_asset_code=USDC&destination_asset_issuer=GA...&amount=100&network=testnet"Response (200):
{
"paths": [
{
"path": [
{
"asset_type": "native"
}
],
"destination_amount": "99.5000000",
"source_amount": "100.0000000"
}
],
"direction": "strict_send"
}Errors:
400: Invalid parameters or no paths found
Compute destination_min or send_max for a selected path with slippage.
Request:
curl -X POST http://localhost:3001/api/v1/simulator/estimate \
-H "Content-Type: application/json" \
-d '{
"path": [...],
"sendAmount": "100.0",
"slippagePercent": 1.5
}'Response (200):
{
"sourceAmount": "100.0000000",
"destinationAmount": "98.5000000"
}Errors:
400: Invalid path or amount
Find paths for a strict send payment (you control the amount sent).
Request:
curl -X POST http://localhost:3001/api/v1/simulator/path-send \
-H "Content-Type: application/json" \
-d '{
"sourceAsset": {...},
"destinationAsset": {...},
"sendAmount": "100.0",
"network": "testnet"
}'Response (200):
{
"paths": [...],
"direction": "strict_send"
}Find paths for a strict receive payment (you control the amount received).
Request:
curl -X POST http://localhost:3001/api/v1/simulator/path-receive \
-H "Content-Type: application/json" \
-d '{
"sourceAsset": {...},
"destinationAsset": {...},
"receiveAmount": "100.0",
"network": "testnet"
}'Response (200):
{
"paths": [...],
"direction": "strict_receive"
}Estimate transaction fee based on current network fee stats.
Query Parameters:
operations(optional, default1): Number of operations in the transactionnetwork(optional, defaulttestnet):mainnetortestnet
Request:
curl "http://localhost:3001/api/v1/simulator/fee?operations=3&network=testnet"Response (200):
{
"baseFee": 100,
"totalFee": 300,
"operations": 3,
"network": "testnet"
}Price several slippage tolerances against a single simulated adverse rate move (#351).
A path payment carries a tolerance rather than a locked rate: destinationMin for strict_send, sendMax for strict_receive. The network fails the operation when the live route cannot fill inside it. This endpoint reads the live route table for a pair and prices up to ten tolerances against one adverse move, so they can be compared directly.
Request body:
direction(required):strict_sendorstrict_receivesourceAsset(required):XLMorCODE:ISSUERdestinationAsset(required):XLMorCODE:ISSUERamount(required): the pinned leg — the source amount forstrict_send, the destination amount forstrict_receive. Up to 15 integer and 7 fractional digitsslippageScenarios(required): 1–10 tolerance percentages, each at least0.01and at most100adverseMovePercent(optional, default0): the deterioration to simulate between quote and landing,0–100routeIndex(optional, default0): which route to simulate, zero-based in the order Horizon returned them.0is the best routenetwork(optional, defaulttestnet):mainnetortestnet
Request:
curl -X POST "http://localhost:3001/api/v1/simulator/path-payment-lab" \
-H "content-type: application/json" \
-d '{
"direction": "strict_send",
"sourceAsset": "XLM",
"destinationAsset": "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHT3VM35KCEIWI6VH5XY4O2Y5JV3CJQ",
"amount": "100.0000000",
"slippageScenarios": [0.1, 0.5, 1, 5],
"adverseMovePercent": 2,
"network": "testnet"
}'Response (200):
{
"network": "testnet",
"direction": "strict_send",
"sourceAsset": "XLM",
"destinationAsset": "USDC:GA5Z…JCJQ",
"routeCount": 2,
"route": {
"index": 0,
"pathLength": 1,
"sourceAmount": "100.0000000",
"destinationAmount": "98.0000000",
"exchangeRate": "0.98",
"fixedAmount": "100.0000000",
"variableAmount": "98.0000000",
"hops": [{ "assetType": "credit_alphanum4", "assetCode": "USDC", "assetIssuer": "GA5Z…JCJQ" }]
},
"comparison": {
"direction": "strict_send",
"guaranteeField": "destinationMin",
"fixedAmount": "100.0000000",
"quotedVariableAmount": "98.0000000",
"adverseMovePercent": 2,
"adverseVariableAmount": "96.0000000",
"scenarios": [
{
"slippagePercent": 0.1,
"guarantee": "97.9020000",
"adverseAmount": "96.0000000",
"headroom": "-1.9020000",
"headroomPercent": -1.9408,
"tolerableMovePercent": 0.1,
"verdict": "fail"
}
],
"tightestSlippagePercent": 0.1,
"widestSlippagePercent": 5,
"recommendedSlippagePercent": 5,
"recommendedHeadroomPercent": 3,
"exceededByEveryScenario": false,
"routeDispersionPercent": null
}
}How the arithmetic works:
destinationMin (strict send) |
sendMax (strict receive) |
|
|---|---|---|
| Guarantee | floor(variable × (1 − s)) |
ceil(variable × (1 + s)) |
| Worst case at the move | floor(variable × (1 − m)) |
ceil(variable × (1 + m)) |
| Headroom | worstCase − guarantee |
guarantee − worstCase |
where s is the tolerance and m the adverse move, both as fractions. The two directions subtract differently because a destinationMin is a floor the fill must stay above while a sendMax is a ceiling it must stay below; in both cases a positive headroom means the payment clears.
verdictispass,fail, orexact.exactmeans the tolerance and the move produced the same amount, so the payment clears only if the rate does not move by another stroop — treat it as a failure.- All amount arithmetic runs on exact stroop integers and rounds the way the network rounds, so a reported
destinationMin/sendMaxis always one the network accepts. No amount is ever held in a floating-point number. recommendedSlippagePercentis the narrowest compared tolerance that still absorbs the move; anything tighter would fail. It isnullandexceededByEveryScenarioistruewhen every compared tolerance is exceeded.routeDispersionPercentreports how far the selected route already sits below the best route, as a percentage of the best one. It isnullwhen route0was simulated.
Limits:
slippageScenarios: 1–10 entries, each0.01–100. A tolerance below0.01%would round to zero hundredths of a percent and mean "no tolerance", so it is rejected rather than silently accepted.adverseMovePercent:0–100.routeIndex:0torouteCount − 1.
Errors:
400: invalid amount, asset format, or tolerance;routeIndexbeyond the routes Horizon returned; no route for the pair429: global rate limit exceeded
Operational notes:
- The call is not cached. A run is a pure function of the live route table plus the caller's tolerances, and any cached answer would describe a route table that has since moved.
POSTanswers200, not201: nothing is created.
Search for liquidity pools by asset pair on Stellar.
Query Parameters:
assetA(required): First asset in the pair. UseXLMfor native orCODE:ISSUERfor non-native.assetB(required): Second asset in the pair. UseXLMfor native orCODE:ISSUERfor non-native.network(optional, defaulttestnet):mainnetortestnet
Request:
curl "http://localhost:3001/api/v1/liquidity-pools/search?assetA=XLM&assetB=USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5&network=testnet"Response (200):
[
{
"poolId": "a468d41d61e...",
"network": "testnet",
"assetA": "native",
"assetB": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
"reserveA": "1000000.0000000",
"reserveB": "500000.0000000",
"totalShares": "707106.7811865",
"feePct": "0.30%",
"totalTrustlines": 42,
"type": "constant_product",
"spotPriceAperB": "2.0000000",
"spotPriceBperA": "0.5000000"
}
]Errors:
400: Invalid asset format or network
Get detailed information about a specific pool.
Query Parameters:
poolId(required): 64-character hex pool IDnetwork(optional, defaulttestnet):mainnetortestnet
Request:
curl "http://localhost:3001/api/v1/liquidity-pools/details?poolId=a468d41d61e...&network=testnet"Response (200):
{
"poolId": "a468d41d61e...",
"network": "testnet",
"assetA": "native",
"assetB": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
"reserveA": "1000000.0000000",
"reserveB": "500000.0000000",
"totalShares": "707106.7811865",
"feePct": "0.30%",
"totalTrustlines": 42,
"type": "constant_product",
"spotPriceAperB": "2.0000000",
"spotPriceBperA": "0.5000000"
}Errors:
400: Invalid pool ID or network404: Pool not found
Calculate the value of LP shares.
Request Body:
{
"poolId": "a468d41d61e...",
"shares": "100.0000000",
"network": "testnet"
}Request:
curl -X POST http://localhost:3001/api/v1/liquidity-pools/share-value \
-H "Content-Type: application/json" \
-d '{
"poolId": "a468d41d61e...",
"shares": "100.0000000",
"network": "testnet"
}'Response (201):
{
"poolId": "a468d41d61e...",
"network": "testnet",
"shares": "100.0000000",
"valueA": "141.4213562",
"valueB": "70.7106781",
"sharePercentage": "0.01414214",
"assetA": "native",
"assetB": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
}Errors:
400: Invalid input or pool state (e.g., empty pool, shares exceed total)404: Pool not found
Add a pool to your watchlist. Requires authentication.
Request Body:
{
"poolId": "a468d41d61e...",
"assetA": "XLM",
"assetB": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
"label": "My XLM/USDC Pool",
"network": "testnet"
}Response (201):
{
"id": "uuid",
"poolId": "a468d41d61e...",
"network": "testnet",
"assetA": "XLM",
"assetB": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
"label": "My XLM/USDC Pool",
"createdAt": "2024-01-01T00:00:00.000Z"
}Errors:
400: Invalid input or pool does not exist401: Authentication required
Remove a pool from your watchlist. Requires authentication.
Request Body:
{
"id": "uuid"
}Response (204): No content
Errors:
401: Authentication required404: Watched pool not found
Get your watched pools. Requires authentication.
Response (200):
[
{
"id": "uuid",
"poolId": "a468d41d61e...",
"network": "testnet",
"assetA": "XLM",
"assetB": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
"label": "My XLM/USDC Pool",
"createdAt": "2024-01-01T00:00:00.000Z"
}
]Errors:
401: Authentication required
Publishes the bounds POST /multisig/simulate accepts, so a client can validate a form before a round trip rather than after a 400 (#352).
Request:
curl "http://localhost:3001/api/v1/multisig/limits"Response (200):
{
"maxSigners": 21,
"maxSignerWeight": 255,
"maxThreshold": 255,
"operationThresholds": [
{ "kind": "low", "gates": "Trustline and offer operations" },
{ "kind": "medium", "gates": "Payments and path payments" },
{ "kind": "high", "gates": "Account settings and clawbacks" }
]
}maxSigners is 21 because SEP-0023 allows 20 additional signers plus the master key, and a request may describe the master key too. maxSignerWeight and maxThreshold are 255 because that is the uint8 the XDR uses.
Evaluate a weighted multisig against the signatures collected so far.
A Stellar multisig is not "2 of 3 signers" — it is any subset of signers whose weights total at least the threshold. This endpoint answers what that collected weight authorises, which weight classes are cleared, which signers are still outstanding, and which configuration risks apply.
Request body:
threshold(required): the weight the operation needs,0–255. This is the account'smediumthreshold, which gates payments and path paymentssigners(required): 1–21 entries, each with:key(required): aG…account idweight(required):0–255signed(optional, defaultfalse): whether a signature from this signer is collectedrequired(optional, defaultfalse): a master-weight-0 required signer. Its weight must be0
lowThreshold/highThreshold(optional): thelowandhighweight classes. Both default tothreshold, which is what Stellar itself defaults them tominTime/maxTime(optional): the transaction validity window, as unix seconds or an ISO 8601 timestamp. Omit or send an empty value for "unbounded"
Request:
curl -X POST "http://localhost:3001/api/v1/multisig/simulate" \
-H "content-type: application/json" \
-d '{
"threshold": 2,
"lowThreshold": 1,
"highThreshold": 3,
"signers": [
{ "key": "GA5ZSEJYB37JRC5AVCIA5MOP4RHT3VM35KCEIWI6VH5XY4O2Y5JV3CJQ", "weight": 2, "signed": true },
{ "key": "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", "weight": 1, "signed": false },
{ "key": "GBRPYHIL3CI3FNQ4BXNFMNDLFJUNSU2HY3ZMFSLONUCEOASW7QC7OX2H", "weight": 1, "signed": false }
]
}'Response (200):
{
"threshold": 2,
"lowThreshold": 1,
"mediumThreshold": 2,
"highThreshold": 3,
"totalWeight": 4,
"signedWeight": 2,
"deficit": 0,
"surplus": 0,
"progressPercent": 100,
"satisfied": true,
"canSubmit": true,
"signers": [
{
"key": "GA5ZSEJYB37JRC5AVCIA5MOP4RHT3VM35KCEIWI6VH5XY4O2Y5JV3CJQ",
"weight": 2,
"signed": true,
"required": false,
"shareOfTotalPercent": 50,
"shareOfThresholdPercent": 100,
"controlsAccount": true,
"indispensable": true,
"redundant": false
}
],
"operationThresholds": [
{ "kind": "low", "requiredWeight": 1, "collectedWeight": 2, "deficit": 0, "cleared": true },
{ "kind": "medium", "requiredWeight": 2, "collectedWeight": 2, "deficit": 0, "cleared": true },
{ "kind": "high", "requiredWeight": 3, "collectedWeight": 2, "deficit": 1, "cleared": false }
],
"outstandingRequiredSigners": [],
"minimumSignersNeeded": [],
"minimumSetWeight": 0,
"duplicateSigners": [],
"risks": [
{
"code": "SINGLE_SIGNER_CONTROLS",
"severity": "warning",
"message": "Signer weight 2 alone reaches the threshold of 2.",
"signers": ["GA5ZSEJYB37JRC5AVCIA5MOP4RHT3VM35KCEIWI6VH5XY4O2Y5JV3CJQ"]
}
],
"timeBounds": {
"minTime": null,
"maxTime": null,
"notYetActive": false,
"expired": false,
"invalid": false
}
}Field notes:
satisfiedissignedWeight >= threshold.canSubmitadditionally requires everyrequiredsigner to have signed — a missing required signer blocks the account regardless of collected weight.progressPercentismin(signedWeight / threshold, 1), and is100when the threshold is0.minimumSignersNeededis the smallest set of outstanding signers that reaches the threshold, chosen on fewest signers and then the tightest fit, so the answer never commits more weight than the quorum needs. It isnullwhen no combination of the remaining signers is enough.nulland[]are therefore different answers: unreachable versus already satisfied.indispensable/redundantdescribe the configured signer set — what happens if that key is removed — whileminimumSignersNeededanswers who still has to sign.duplicateSignerslists keys that appear more than once. The totals include them because the request did, but Stellar counts each key once; the finding is reported rather than silently deduplicated.
Risk codes:
| Code | Severity | Meaning |
|---|---|---|
THRESHOLD_ZERO |
critical | A threshold of 0 authorises the operation without any signature |
THRESHOLD_UNREACHABLE |
critical | No signer carries weight, so no signature set can reach a threshold of 1 or more |
THRESHOLD_ABOVE_TOTAL_WEIGHT |
critical | Total weight is below the threshold |
REQUIRED_SIGNER_UNSIGNED |
critical | A required signer has not signed, so the account cannot be modified |
SINGLE_SIGNER_CONTROLS |
warning | One signer's weight alone reaches the threshold |
REQUIRES_EVERY_SIGNER |
warning | Removing any weighted signer drops the account below the threshold |
QUORUM_SINGLE_POINT_OF_FAILURE |
warning | One outstanding signature completes the quorum, so that signer can stall the account alone |
DUPLICATE_SIGNER |
warning | The same key appears more than once |
ZERO_WEIGHT_SIGNERS |
info | Zero-weight signers are recorded but add no weight |
REDUNDANT_SIGNER |
info | These signers could be dropped without weakening the quorum |
Limits:
signers: 1–21 entriesweight,threshold,lowThreshold,highThreshold: integers0–255minTime/maxTime: unix seconds or ISO 8601;minTimemust not be aftermaxTime
Errors:
400: weight or threshold outside0–255, more than 21 signers, arequiredsigner carrying weight, a window that closes before it opens, or an unreadable timestamp429: global rate limit exceeded
Operational notes:
- The call is stateless: nothing is fetched and nothing is stored. The same request always yields the same answer, so it is safe to try configurations that do not exist.
POSTanswers200, not201: nothing is created.
List all supported operation types with field schemas.
Request:
curl http://localhost:3001/api/v1/composer/operationsResponse (200):
{
"operations": [
{
"type": "payment",
"description": "Send an asset to another account",
"fields": [
{
"name": "destination",
"type": "string",
"description": "Destination account public key",
"required": true
},
{
"name": "asset",
"type": "object",
"description": "Asset to send"
},
{
"name": "amount",
"type": "string",
"description": "Amount to send"
}
]
},
{
"type": "path_payment_strict_send",
"description": "Send an asset via a specific path",
"fields": [...]
}
]
}Build a multi-op transaction and return unsigned XDR envelope.
Request:
curl -X POST http://localhost:3001/api/v1/composer/build \
-H "Content-Type: application/json" \
-d '{
"sourceAccount": {
"publicKey": "GBZR7...",
"sequence": "1234567890"
},
"fee": "300",
"operations": [
{
"type": "payment",
"destination": "GBUQWP...",
"asset": "native",
"amount": "10.00"
}
],
"network": "testnet"
}'Response (200):
{
"xdr": "AAAAAgAAAAB+Ht3sW...",
"hash": "5fa...",
"envelope_type": "ENVELOPE_TYPE_TX"
}Errors:
400: Invalid transaction parameters
Dry-run an XDR transaction against Horizon; returns fee and result codes.
Request:
curl -X POST http://localhost:3001/api/v1/composer/simulate \
-H "Content-Type: application/json" \
-d '{
"xdr": "AAAAAgAAAAB+Ht3sW...",
"network": "testnet"
}'Response (200):
{
"resultXdr": "...",
"fee": "300",
"resultCode": "txSUCCESS",
"operationResults": [
{
"code": "opSUCCESS"
}
]
}Errors:
400: Invalid XDR or simulation failed
Fetch, decode, and inspect a Stellar transaction by hash.
Query Parameters:
network(optional, defaulttestnet):testnetormainnet
Request:
curl "http://localhost:3001/api/v1/inspector/tx/5fa1f6d8a7c..."Response (200):
{
"hash": "5fa1f6d8a7c...",
"ledger": 12345,
"createdAt": "2024-06-21T12:34:56Z",
"sourceAccount": "GBZR7...",
"sequenceNumber": "1234567890",
"feeCharged": "300",
"maxFee": "300",
"memo": null,
"memoType": "none",
"timeBounds": null,
"signatures": ["..."],
"success": true,
"resultCode": "tx_success",
"resultExplanation": "The transaction was code-path complete and succeeded.",
"operationCount": 1,
"operations": [
{
"type": "payment",
"fields": {
"destination": "GBUQWP...",
"amount": "10.00",
"asset": "XLM"
},
"index": 0,
"resultCode": "op_success",
"resultExplanation": "The payment operation succeeded.",
"success": true,
"effects": []
}
],
"rawJson": {},
"network": "testnet",
"composerPayload": {}
}Errors:
404: Transaction not found
Export a transaction breakdown as CSV (UTF-8 BOM included for Excel compatibility).
Query Parameters:
network(optional, defaulttestnet):testnetormainnet
Request:
curl "http://localhost:3001/api/v1/inspector/tx/5fa1f6d8a7c.../export?network=testnet"Response (200): text/csv attachment
hash,network,ledger,created_at,source_account,sequence_number,fee_charged,max_fee,memo,memo_type,success,result_code,result_explanation,operation_index,operation_type,operation_label,operation_source,operation_result_code,operation_success,operation_effects,operation_fields
Errors:
404: Transaction not found
Get the last 20 transactions for a Stellar account.
Query Parameters:
network(optional, defaulttestnet):testnetormainnet
Request:
curl "http://localhost:3001/api/v1/inspector/account/GBZR7.../txs"Response (200):
[
{
"hash": "5fa1f6d8a7c...",
"createdAt": "2024-06-21T12:34:56Z",
"operationCount": 1,
"feeCharged": "300",
"success": true,
"resultCode": "tx_success"
}
]Errors:
404: Account not found
Decode raw Stellar XDR envelope (offline, no Horizon network call required).
Request:
curl -X POST http://localhost:3001/api/v1/inspector/decode-xdr \
-H "Content-Type: application/json" \
-d '{
"xdr": "AAAAAgAAAAB+Ht3sW...",
"network": "testnet"
}'Response (200):
{
"hash": "5fa1f6d8a7c...",
"ledger": 0,
"createdAt": "",
"sourceAccount": "GBZR7...",
"sequenceNumber": "1234567890",
"feeCharged": "0",
"maxFee": "300",
"memo": null,
"memoType": "none",
"timeBounds": null,
"signatures": ["..."],
"success": true,
"resultCode": "tx_success",
"resultExplanation": "Transaction decoded from XDR — not yet submitted.",
"operationCount": 1,
"operations": [
{
"type": "payment",
"fields": {
"destination": "GBUQWP...",
"amount": "10.00",
"asset": "XLM"
},
"index": 0,
"resultCode": null,
"resultExplanation": null,
"success": true,
"effects": []
}
],
"rawJson": null,
"network": "testnet",
"composerPayload": {}
}Errors:
400: Invalid XDR
Get current Stellar network status and fees.
Query Parameters:
network(optional, defaultmainnet):mainnetortestnet
Request:
curl "http://localhost:3001/api/v1/network/status?network=testnet"Response (200):
{
"network": "testnet",
"baseFee": 100,
"baseReserve": 0.5,
"protocolVersion": 21,
"timestamp": "2024-06-21T12:34:56Z"
}Get last 60 minutes of network status history.
Query Parameters:
network(optional, defaultmainnet):mainnetortestnet
Request:
curl "http://localhost:3001/api/v1/network/status/history?network=testnet"Response (200):
{
"network": "testnet",
"history": [
{
"timestamp": "2024-06-21T11:34:56Z",
"baseFee": 100,
"baseReserve": 0.5
},
{
"timestamp": "2024-06-21T12:34:56Z",
"baseFee": 100,
"baseReserve": 0.5
}
]
}Fetch and decode Soroban events for a contract. This read-only endpoint does not require authentication.
Query parameters: contractId (required), network (testnet or mainnet, default testnet), type (contract, system, or diagnostic), startLedger or cursor (mutually exclusive), endLedger, and limit (1–200).
Filter decoded events in memory. The request accepts up to 1,000 events and 10 criteria. Criteria are ANDed. Text criteria (topic_contains, value_type_is, value_equals) require a non-empty value of at most 256 characters. A ledger_range requires from or to; supplied bounds must be non-negative safe integers and from must not exceed to. Invalid criteria return 400.
{
"events": [],
"criteria": [
{ "kind": "topic_contains", "value": "transfer" },
{ "kind": "ledger_range", "from": 100, "to": 200 }
]
}Replay filtered events to a webhook. This endpoint requires authentication; URLs are checked against SSRF protections. See the Contract Events guide.
Deploy a Soroban smart contract from a WASM file.
Request (multipart/form-data):
curl -X POST http://localhost:3001/api/v1/contracts/deploy \
-F "file=@contract.wasm" \
-F "args=[\"arg1\",\"arg2\"]"Response (200):
{
"contractId": "CABC...",
"deployTransactionHash": "5fa1f6d...",
"wasmHash": "9e5551...",
"network": "testnet"
}Errors:
400: Invalid WASM file or deployment failed
Invoke a contract function.
Request:
curl -X POST http://localhost:3001/api/v1/contracts/CABC.../invoke \
-H "Content-Type: application/json" \
-d '{
"functionName": "transfer",
"args": ["GBU...", "100.00"]
}'Response (200):
{
"result": "...",
"transactionHash": "5fa1f6d..."
}Errors:
400: Invalid contract ID or parameters
Get contract metadata from the network.
Request:
curl http://localhost:3001/api/v1/contracts/CABC.../infoResponse (200):
{
"contractId": "CABC...",
"wasmHash": "9e5551...",
"createdLedger": 12345,
"createdAt": "2024-06-21T12:34:56Z"
}Errors:
404: Contract not found
List all supported webhook event types with schemas and sample payloads.
Request:
curl http://localhost:3001/api/v1/webhooks/templatesResponse (200):
[
{
"eventType": "transaction.submitted",
"description": "Emitted when a transaction is submitted",
"schema": {},
"samplePayload": {}
}
]Send a webhook payload to a target endpoint. Requires authentication.
Request:
curl -X POST http://localhost:3001/api/v1/webhooks/send \
-H "Content-Type: application/json" \
--cookie "savitools_access_token=YOUR_ACCESS_TOKEN" \
-d '{
"endpointUrl": "https://example.com/webhook",
"eventType": "transaction.submitted",
"payload": {}
}'Response (201): a WebhookHistoryEntry (see /webhooks/history).
{
"id": "1f0c...",
"eventType": "transaction.submitted",
"endpointUrl": "https://example.com/webhook",
"method": "POST",
"requestHeaders": {
"Content-Type": "application/json"
},
"payload": {},
"responseStatus": 200,
"responseBody": "ok",
"latencyMs": 250,
"timestamp": 1717243200000
}signature.body is byte-for-byte the request body that was sent and signed, so a receiver (or
the Webhook Tester UI) can recompute the identical HMAC from signature.timestamp and
signature.body without guessing the serialisation. The signature value in
requestHeaders is redacted before storage; signature.signature carries the value that went
on the wire.
Errors:
400: Invalid webhook payload or an unsafe destination502: Request payload exceeds the size limit, or the destination failed
Get the last 50 webhook send attempts. Requires authentication.
Request:
curl http://localhost:3001/api/v1/webhooks/history \
--cookie "savitools_access_token=YOUR_ACCESS_TOKEN"Response (200):
[
{
"id": "1f0c...",
"eventType": "transaction.submitted",
"endpointUrl": "https://example.com/webhook",
"method": "POST",
"requestHeaders": {"X-SaviTools-Timestamp": "1717243200"},
"payload": {...},
"statusCode": 200,
"responseStatus": 200,
"responseBody": "ok",
"latencyMs": 250,
"timestamp": 1717243200000
}
]Entries recorded under the legacy body-only signing format are returned with
"legacySignature": true.
Replay a previous webhook send attempt. Requires authentication.
The stored secret-shaped headers are redacted and cannot be reconstructed, so the replay is
signed afresh with the deployment-wide WEBHOOK_SIGNING_SECRET (or sent unsigned if none is
configured). Any recorded signing header is dropped first, so the replay never carries a
timestamp that disagrees with the signature beside it.
Request:
curl -X POST http://localhost:3001/api/v1/webhooks/replay/1f0c... \
--cookie "savitools_access_token=YOUR_ACCESS_TOKEN"Response (201): a new WebhookHistoryEntry, as returned by /webhooks/send.
Errors:
404: Webhook attempt not found
Fetch and cache an OpenAPI spec for a provider (requires authentication).
Request:
curl http://localhost:3001/api/v1/playground/spec/stripe \
--cookie "access_token=YOUR_ACCESS_TOKEN"Response (200):
{
"provider": "stripe",
"spec": {...}
}Errors:
404: Provider spec not found401: Not authenticated
Proxy a request to the target API with server-side auth (requires authentication).
Request:
curl -X POST http://localhost:3001/api/v1/playground/proxy \
-H "Content-Type: application/json" \
--cookie "access_token=YOUR_ACCESS_TOKEN" \
-d '{
"provider": "stripe",
"method": "GET",
"path": "/v1/customers",
"params": {}
}'Response (200):
{
"statusCode": 200,
"body": {...}
}Save an encrypted API key (requires authentication).
Request:
curl -X POST http://localhost:3001/api/v1/playground/keys \
-H "Content-Type: application/json" \
--cookie "access_token=YOUR_ACCESS_TOKEN" \
-d '{
"provider": "stripe",
"key": "sk_live_..."
}'Response (201):
{
"id": "key-123",
"provider": "stripe",
"keyMasked": "sk_live_...***"
}List stored API keys (masked, requires authentication).
Request:
curl http://localhost:3001/api/v1/playground/keys \
--cookie "access_token=YOUR_ACCESS_TOKEN"Response (200):
{
"keys": [
{
"id": "key-123",
"provider": "stripe",
"keyMasked": "sk_live_...***"
}
]
}Update a stored API key (requires authentication).
Request:
curl -X PUT http://localhost:3001/api/v1/playground/keys/key-123 \
-H "Content-Type: application/json" \
--cookie "access_token=YOUR_ACCESS_TOKEN" \
-d '{
"key": "sk_live_new..."
}'Response (200):
{
"id": "key-123",
"keyMasked": "sk_live_...***"
}Delete a stored API key (requires authentication).
Request:
curl -X DELETE http://localhost:3001/api/v1/playground/keys/key-123 \
--cookie "access_token=YOUR_ACCESS_TOKEN"Response (204): No content
Get persisted tool state for the current user (requires authentication).
Path Parameters:
tool:sandbox|inspector|webhooks|composer
Request:
curl http://localhost:3001/api/v1/workspaces/composer \
--cookie "access_token=YOUR_ACCESS_TOKEN"Response (200):
{
"tool": "composer",
"data": {...}
}Errors:
400: Invalid tool name401: Not authenticated
Save tool state for the current user (requires authentication).
Request:
curl -X PUT http://localhost:3001/api/v1/workspaces/composer \
-H "Content-Type: application/json" \
--cookie "access_token=YOUR_ACCESS_TOKEN" \
-d '{
"data": {...}
}'Response (200):
{
"tool": "composer",
"data": {...}
}Create a watch for an account or contract (requires authentication).
Request:
curl -X POST http://localhost:3001/api/v1/monitor/watches \
-H "Content-Type: application/json" \
--cookie "access_token=YOUR_ACCESS_TOKEN" \
-d '{
"address": "GBZR7WLLV5OZVUQ4WAWCKVCOVWGZFZVHG5GMRFYVZJZ2AFSGHFKDQ4C",
"type": "account",
"label": "My Account",
"network": "testnet"
}'Response (201):
{
"id": "watch-123",
"address": "GBZR7...",
"type": "account",
"label": "My Account"
}Get all watches for the current user (requires authentication).
Request:
curl http://localhost:3001/api/v1/monitor/watches \
--cookie "access_token=YOUR_ACCESS_TOKEN"Response (200):
{
"watches": [
{
"id": "watch-123",
"address": "GBZR7...",
"type": "account",
"label": "My Account"
}
]
}Delete a watch (requires authentication).
Request:
curl -X DELETE http://localhost:3001/api/v1/monitor/watches/watch-123 \
--cookie "access_token=YOUR_ACCESS_TOKEN"Response (204): No content
Get alerts for a watch (requires authentication).
Request:
curl http://localhost:3001/api/v1/monitor/watches/watch-123/alerts \
--cookie "access_token=YOUR_ACCESS_TOKEN"Response (200):
[
{
"id": "alert-456",
"watchId": "watch-123",
"conditionType": "balance_threshold"
}
]Search watch events across the current user's watches (requires authentication). Accepts the same filters as the CSV export endpoint.
Query Parameters:
watchId(optional): Restrict to a single watcheventType(optional):transaction,payment, orcontractq(optional): Free-text search across event payloads (hashes, accounts, assets)from(optional): ISO date — events at or after this timeto(optional): ISO date — events at or before this timepage(optional, default1)limit(optional, default25, max100)
Request:
curl "http://localhost:3001/api/v1/monitor/search?eventType=payment&q=GBZR7...&limit=50" \
--cookie "access_token=YOUR_ACCESS_TOKEN"Response (200):
{
"items": [
{
"id": "...",
"watchId": "...",
"eventType": "payment",
"payload": {},
"occurredAt": "2026-08-31T12:00:00.000Z"
}
],
"page": 1,
"limit": 50,
"total": 1
}Export monitor search results as CSV (requires authentication). Accepts the exact same query parameters as GET /monitor/search. The response is a text/csv attachment with a UTF-8 BOM; large result sets are streamed in chunks and capped at 10000 rows.
Request:
curl "http://localhost:3001/api/v1/monitor/search/export?eventType=payment&limit=10000" \
--cookie "access_token=YOUR_ACCESS_TOKEN" \
-o monitor-search.csvResponse (200): text/csv attachment
event_type,occurred_at,amount,asset,from,to,transaction_hash,paging_token,watch_id,payload
Generate SDK code from a provider spec.
Request:
curl -X POST http://localhost:3001/api/v1/sdkgen/generate \
-H "Content-Type: application/json" \
-d '{
"spec": "fluxa",
"language": "typescript",
"endpoint": "https://api.example.com"
}'Response (200):
{
"code": "// Generated TypeScript SDK\nimport axios from 'axios';\n..."
}| Code | Meaning | When It Occurs |
|---|---|---|
200 |
OK | Successful GET, POST, or PUT request |
201 |
Created | Successful resource creation (POST) |
204 |
No Content | Successful DELETE request |
400 |
Bad Request | Invalid request parameters or validation failed |
401 |
Unauthorized | Missing or invalid authentication token |
404 |
Not Found | Resource does not exist |
422 |
Unprocessable Entity | Semantic error in request (e.g., invalid WASM) |
500 |
Internal Server Error | Unexpected server error |
Meaning: The Stellar public key provided is malformed or invalid.
Suggested Resolution: Verify the public key format (starts with G, 56 characters). Use /wallet/generate if unsure.
Meaning: The source account doesn't have enough native asset to cover the transaction fee and amount.
Suggested Resolution: Use /wallet/fund to add testnet funds, or send a smaller amount.
Meaning: Email/password combination is incorrect. Suggested Resolution: Double-check your email and password. Register a new account if needed.
Meaning: Your access token has expired (default 15 minutes).
Suggested Resolution: Call POST /auth/refresh with your refresh token to get a new access token.
Meaning: The specified transaction hash doesn't exist on the network. Suggested Resolution: Verify the transaction hash is correct and the network (mainnet/testnet) is correct.
Meaning: The uploaded file is not a valid Soroban WASM binary.
Suggested Resolution: Ensure the file is a compiled .wasm file from a Soroban contract.
SaviTools proxies some errors directly from the Stellar Horizon API. These errors include:
op_no_trust: Destination account doesn't have a trustline for the assetop_line_full: Destination account's limit for the asset is at maxop_underfunded: Source account doesn't have enough fundstx_bad_seq: Transaction sequence number is incorrecttx_bad_auth: Transaction hasn't been signed by the required signers
Example Horizon Error Response:
{
"type": "https://stellar.org/horizon-errors/transaction-failed",
"title": "Transaction Failed",
"status": 400,
"detail": "...",
"extras": {
"envelope_xdr": "...",
"result_xdr": "...",
"result_codes": {
"transaction": "tx_failed",
"operations": ["op_no_trust"]
}
}
}For a complete list, refer to the Stellar Horizon API documentation.
Caching is not currently implemented in the API; all requests are processed dynamically against upstream services and databases.
Current Status: Rate limiting is enforced globally across all endpoints via NestJS ThrottlerGuard according to application configuration.
- CORS Origin: Controlled by
WEB_ORIGINenvironment variable (default:http://localhost:3000) - HTTPS: Enforced in production; cookies marked with
Secureflag - Session Security: HTTP-only cookies store authentication tokens securely.
- Input Validation: All inputs are validated and sanitized server-side
- API Status: Check Stellar Horizon Status
- Bug Reports: GitHub Issues
- Questions: Refer to Stellar Docs
Fetches an authentication challenge from a SEP-10 server.
Request:
curl -X POST http://localhost:3001/api/v1/sep10/fetch-challenge \
-H "Content-Type: application/json" \
-d '{
"webAuthEndpoint": "https://testanchor.stellar.org/auth",
"clientAccountId": "GABC...",
"homeDomain": "testanchor.stellar.org"
}'Response (200):
{
"transaction": "AAAAAgAAAA...",
"network_passphrase": "Test SDF Network ; September 2015",
"parsed": {
"source": "GABC...",
"sequence": "0"
}
}Validates a SEP-10 challenge transaction.
Request:
curl -X POST http://localhost:3001/api/v1/sep10/validate-challenge \
-H "Content-Type: application/json" \
-d '{
"challengeXdr": "AAAAAgAAAA...",
"serverSigningKey": "GABC...",
"network": "testnet"
}'Response (200):
{
"isValid": true,
"clientAccountId": "GABC...",
"timeBounds": {
"minTime": "1234567890",
"maxTime": "1234567990",
"isValid": true
},
"issues": []
}Signs a challenge transaction with a keypair.
Request:
curl -X POST http://localhost:3001/api/v1/sep10/sign-challenge \
-H "Content-Type: application/json" \
-d '{
"challengeXdr": "AAAAAgAAAA...",
"signerSecretKey": "SABC...",
"network": "testnet"
}'Response (200):
{
"signedTransaction": "AAAAAgAAAA..."
}Exchanges a signed challenge for a JWT token.
Request:
curl -X POST http://localhost:3001/api/v1/sep10/token-exchange \
-H "Content-Type: application/json" \
-d '{
"webAuthEndpoint": "https://testanchor.stellar.org/auth",
"signedChallengeXdr": "AAAAAgAAAA..."
}'Response (200):
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Queries contract storage for a specific key.
Request:
curl -X POST http://localhost:3001/api/v1/soroban-storage/query \
-H "Content-Type: application/json" \
-d '{
"contractId": "CABC...",
"key": "balance",
"network": "testnet",
"keyType": "symbol"
}'Response (200):
{
"key": "balance",
"value": {
"type": "u128",
"value": "1000000"
},
"lastModified": 12345
}Compares storage between two contracts.
Request:
curl -X POST http://localhost:3001/api/v1/soroban-storage/compare \
-H "Content-Type: application/json" \
-d '{
"contractId1": "CABC...",
"contractId2": "CDEF...",
"key": "balance",
"network": "testnet"
}'Response (200):
{
"key": "balance",
"contract1": {
"value": { "type": "u128", "value": "1000000" },
"exists": true
},
"contract2": {
"value": { "type": "u128", "value": "2000000" },
"exists": true
},
"differences": [...]
}Generates a properly typed storage key.
Request:
curl -X POST http://localhost:3001/api/v1/soroban-storage/typed-key \
-H "Content-Type: application/json" \
-d '{
"keyType": "map",
"keyComponents": [
{ "type": "symbol", "value": "balances" },
{ "type": "address", "value": "GABC..." }
]
}'Response (200):
{
"key": "AAAADwAAAAhiYWxhbmNlcwAAAAEAAAATAAAA...",
"components": [...]
}Parses and validates stellar.toml content.
Request:
curl -X POST http://localhost:3001/api/v1/stellar-toml/parse \
-H "Content-Type: application/json" \
-d '{
"content": "VERSION=\"2.0.0\"\nNETWORK_PASSPHRASE=\"Test SDF Network ; September 2015\"",
"strict": true
}'Response (200):
{
"parsed": {
"VERSION": "2.0.0",
"NETWORK_PASSPHRASE": "Test SDF Network ; September 2015"
},
"issues": [],
"isValid": true
}Formats stellar.toml content.
Request:
curl -X POST http://localhost:3001/api/v1/stellar-toml/format \
-H "Content-Type: application/json" \
-d '{
"content": "VERSION=\"2.0.0\"\n[DOCUMENTATION]\nORG_NAME=\"Example\"",
"indent": "spaces",
"indentSize": 2
}'Response (200):
{
"formatted": "VERSION = \"2.0.0\"\n\n[DOCUMENTATION]\nORG_NAME = \"Example\""
}Validates stellar.toml against SEP-1.
Request:
curl -X POST http://localhost:3001/api/v1/stellar-toml/validate \
-H "Content-Type: application/json" \
-d '{
"content": "VERSION=\"2.0.0\"",
"level": "strict",
"network": "testnet"
}'Response (200):
{
"isValid": true,
"issues": [...],
"summary": {
"errors": 0,
"warnings": 1,
"infos": 0
}
}Gets a pre-configured template.
Query Parameters:
type:minimal,anchor,issuer, orvalidator
Request:
curl "http://localhost:3001/api/v1/stellar-toml/template?type=anchor"Response (200):
{
"template": "VERSION=\"2.0.0\"\n..."
}Gets the current sequence number for an account.
Query Parameters:
account: Account addressnetwork:testnetormainnet
Request:
curl "http://localhost:3001/api/v1/sequence-planner/account-sequence?account=GABC...&network=testnet"Response (200):
{
"account": "GABC...",
"currentSequence": "12345",
"nextSequence": "12346"
}Validates a proposed sequence number.
Request:
curl -X POST http://localhost:3001/api/v1/sequence-planner/validate-sequence \
-H "Content-Type: application/json" \
-d '{
"account": "GABC...",
"proposedSequence": 12346,
"network": "testnet"
}'Response (200):
{
"isValid": true,
"currentSequence": "12345",
"nextValidSequence": "12346",
"gap": 0,
"issues": ["Sequence number is valid and ready to use"]
}Plans sequences for multiple transactions with conflict detection.
Request:
curl -X POST http://localhost:3001/api/v1/sequence-planner/plan \
-H "Content-Type: application/json" \
-d '{
"transactions": [
{
"id": "payment-1",
"sourceAccount": "GABC...",
"description": "Payment transaction"
}
],
"network": "testnet"
}'Response (200):
{
"plannedTransactions": [...],
"conflicts": [],
"accountSequences": [...],
"summary": {
"total": 1,
"valid": 1,
"conflicts": 0,
"warnings": 0
}
}All endpoints return consistent error responses:
{
"statusCode": 400,
"message": "Error description",
"error": "BadRequest"
}Common status codes:
200: Success201: Created400: Bad Request (invalid parameters)401: Unauthorized (authentication required)404: Not Found500: Internal Server Error
The API enforces rate limiting via throttling:
- Default: 100 requests per 60 seconds per IP
- Configurable via
THROTTLE_LIMITandTHROTTLE_TTLenvironment variables
Rate limit headers:
X-RateLimit-Limit: Maximum requests per windowX-RateLimit-Remaining: Remaining requestsX-RateLimit-Reset: Time when the limit resets
For API support:
- Documentation: https://docs.savitools.dev
- GitHub Issues: https://github.com/your-org/savitools/issues
- Email: support@savitools.dev