From zero to a submitted Stellar transaction in 5 minutes.
This guide walks you through the entire workflow using the SaviTools API on testnet. We'll:
- Generate a keypair
- Fund the account
- Check balances
- Find a payment path
- Build a transaction
- Submit it
All commands use curl. For Windows PowerShell users, some escaping may differ.
curlinstalled- A running SaviTools API server (default:
http://localhost:3001) - Testnet environment (queries
testnet-api.savitools.comor local dev)
Generate a new Stellar keypair (public key + secret).
curl -X POST http://localhost:3001/api/v1/wallet/generateExpected Response:
{
"publicKey": "GBZR7WLLV5OZVUQ4WAWCKVCOVWGZFZVHG5GMRFYVZJZ2AFSGHFKDQ4C",
"secret": "SBUQ54DRQG5Q3QLQHJEZ5ODSLGEYZIJEDYAJBSJUKAUJL4MQAQKF3PZ"
}Save these values:
export PUBLIC_KEY="GBZR7WLLV5OZVUQ4WAWCKVCOVWGZFZVHG5GMRFYVZJZ2AFSGHFKDQ4C"
export SECRET="SBUQ54DRQG5Q3QLQHJEZ5ODSLGEYZIJEDYAJBSJUKAUJL4MQAQKF3PZ"What could go wrong:
- API server is not running → Start it with
npm run dev - Network timeout → Check your internet connection
Use the Friendbot service to fund your account with 10 XLM.
curl -X POST http://localhost:3001/api/v1/wallet/fund \
-H "Content-Type: application/json" \
-d "{\"publicKey\": \"$PUBLIC_KEY\"}"Expected Response:
{
"success": true,
"amount": "10.0000000",
"currency": "XLM",
"transactionHash": "6c1e1f6fe8c9b2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c"
}What could go wrong:
400 Bad Requestwith "Invalid public key" → Verify thePUBLIC_KEYexport- Friendbot rate-limited → Wait a few minutes and retry
- Account already exists → Safe to proceed to Step 3
Verify your account now has 10 XLM.
curl "http://localhost:3001/api/v1/wallet/balances?publicKey=$PUBLIC_KEY"Expected Response:
{
"balances": [
{
"asset_type": "native",
"balance": "9.9999800",
"asset_code": "XLM"
}
]
}What could go wrong:
400 Bad Request→ Check that$PUBLIC_KEYis set and valid- Empty balances array → Account may not exist yet; retry Step 2
Note: The balance is 9.99998 instead of 10 because the funding transaction fee (0.0001 XLM) was deducted.
Let's say we want to send 5 XLM to a receiving address. First, find a payment path (there's only one for native XLM).
RECIPIENT="GBUQWPFZ2AEFL3YZWP7CLBX6FAUYGMJC52YTKF4KIWNDWVXMXWBP2C5"
curl "http://localhost:3001/api/v1/simulator/paths?direction=strict_send&source_asset_type=native&destination_asset_type=native&amount=5&network=testnet"Expected Response:
{
"paths": [
{
"source_amount": "5.0000000",
"destination_amount": "5.0000000",
"path": []
}
],
"direction": "strict_send"
}What could go wrong:
400 Bad Request→ Verify query parameters are correct- Empty
pathsarray → Asset pair has no available path (use different assets)
Notes:
- For native-to-native transfers, the path is empty (direct transfer)
- For cross-asset transfers, paths show intermediary hops
Get the current base fee for the network.
curl "http://localhost:3001/api/v1/simulator/fee?operations=1&network=testnet"Expected Response:
{
"baseFee": 100,
"totalFee": 100,
"operations": 1,
"network": "testnet"
}What to do with this:
- Base fee per operation: 100 stroops (0.00001 XLM)
- Total fee for 1 operation: 100 stroops
Build an unsigned transaction to send 5 XLM.
curl -X POST http://localhost:3001/api/v1/composer/build \
-H "Content-Type: application/json" \
-d "{
\"sourceAccount\": {
\"publicKey\": \"$PUBLIC_KEY\",
\"sequence\": \"1\"
},
\"fee\": \"100\",
\"operations\": [
{
\"type\": \"payment\",
\"destination\": \"$RECIPIENT\",
\"asset\": \"native\",
\"amount\": \"5.00\"
}
],
\"network\": \"testnet\"
}"Expected Response:
{
"xdr": "AAAAAgAAAAB+Ht3sW/xvHrHnXJ...",
"hash": "5fa1f6d8a7c9b2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"envelope_type": "ENVELOPE_TYPE_TX"
}Save the XDR:
export XDR="AAAAAgAAAAB+Ht3sW/xvHrHnXJ..."What could go wrong:
400 Bad Requestwith "Invalid source account" → Sequence number may be wrong- Recipient public key invalid → Double-check the
$RECIPIENTformat (starts withG, 56 chars)
Dry-run the transaction to check for errors before submitting.
curl -X POST http://localhost:3001/api/v1/composer/simulate \
-H "Content-Type: application/json" \
-d "{
\"xdr\": \"$XDR\",
\"network\": \"testnet\"
}"Expected Response:
{
"resultXdr": "AAAAAAAAAGQ...",
"fee": "100",
"resultCode": "txSUCCESS",
"operationResults": [
{
"code": "opSUCCESS"
}
]
}What to look for:
"resultCode": "txSUCCESS"✅ Transaction is valid"operationResults": [{"code": "opSUCCESS"}]✅ All operations passed- Any code starting with
tx_orop_= error (see Error Reference)
What could go wrong:
txLATE_LEDGER_CLOSE→ Sequence number changed; rebuild the transactiontxFAILED→ CheckoperationResultsfor details
Important: SaviTools does NOT sign transactions with your secret key. You must sign externally for security.
Use the Stellar CLI or a wallet SDK:
stellar tx sign --network testnet \
--signer "$SECRET" \
--input-xdr "$XDR" \
--output-xdr signed-tx.xdrThen submit to Horizon:
stellar tx submit --network testnet --input-xdr signed-tx.xdrOr use a wallet SDK like JS-Stellar-SDK:
npm install stellar-sdkconst StellarSdk = require('stellar-sdk');
const keypair = StellarSdk.Keypair.fromSecret('SBUQ54...');
const tx = StellarSdk.TransactionBuilder.fromXDR('AAAAAgAAAAB...', StellarSdk.Networks.TESTNET_NETWORK_PASSPHRASE);
tx.sign(keypair);
const envelope = tx.toEnvelope();
// Submit to Horizon
const server = new StellarSdk.Server('https://horizon-testnet.stellar.org');
server.submitTransaction(envelope).then(result => {
console.log('Transaction successful:', result.hash);
}).catch(error => {
console.error('Submission failed:', error);
});What could go wrong:
- Signature invalid → Verify the secret key matches the public key
- Bad envelope XDR → Rebuild the transaction with correct parameters
Once submitted, check the transaction status:
TX_HASH="5fa1f6d8a7c9b2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
curl "http://localhost:3001/api/v1/inspector/tx/$TX_HASH"Expected Response:
{
"hash": "5fa1f6d8a7c9b2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"ledger": 12345,
"createdAt": "2024-06-21T12:34:56Z",
"sourceAccount": "GBZR7WLLV5OZVUQ4WAWCKVCOVWGZFZVHG5GMRFYVZJZ2AFSGHFKDQ4C",
"sequenceNumber": "1",
"feeCharged": "100",
"maxFee": "100",
"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": "GBUQWPFZ2AEFL3YZWP7CLBX6FAUYGMJC52YTKF4KIWNDWVXMXWBP2C5",
"amount": "5.00",
"asset": "XLM"
},
"index": 0,
"resultCode": "op_success",
"resultExplanation": "The payment operation succeeded.",
"success": true,
"effects": []
}
],
"rawJson": {},
"network": "testnet",
"composerPayload": {}
}Verify:
"success": true✅ Payment was confirmed"feeCharged": "100"✅ Correct fee deducted"operations[0].fields.amount": "5.00"✅ Correct amount sent
| Issue | Cause | Solution |
|---|---|---|
curl: (7) Failed to connect |
API not running | Start with npm run dev in apps/api |
401 Unauthorized |
Using an authenticated endpoint | Public endpoints don't require auth; check endpoint docs |
400 Invalid public key |
Malformed key | Use /wallet/generate or validate key format |
404 Not found |
Transaction doesn't exist yet | Wait a few seconds and retry; check hash spelling |
ENOVEL_TIME result code |
Ledger closed before submission | Increase maxTime or retry immediately |
| Signature invalid | Secret key doesn't match public key | Verify both are from the same /wallet/generate call |
Now that you've submitted a transaction, explore:
-
Multi-Operation Transactions: Build transactions with multiple operations (e.g., payments + trades)
- Check
GET /composer/operationsfor all op types - See
/composer/builddocs for examples
- Check
-
Cross-Asset Swaps: Use
/simulator/pathsto trade between different assets- Use
path_payment_strict_sendorpath_payment_strict_receiveoperation types
- Use
-
Soroban Smart Contracts: Deploy and invoke contracts
/contracts/deployfor WASM files/contracts/:id/invokefor function calls
-
Webhook Integration: Listen for transaction events
- Check
/webhooks/templatesfor supported events - Use
/webhooks/sendfor testing
- Check
-
User Workspaces: Persist UI state across sessions
GET /workspaces/:toolto read saved statePUT /workspaces/:toolto save state
For detailed endpoint docs, responses, and all parameters:
- 📖 Full API Reference: See
/api/docs(Swagger UI) ordocs/api-reference.md - 🚀 Stellar Docs: https://developers.stellar.org/docs
- 💬 Community Discord: https://discord.gg/stellar
Found an issue with this guide? Report it on GitHub