Embed digital Tanzanian Shilling wallets directly into your product. Create users, accept M-Pesa deposits, send peer-to-peer transfers, and cash out to mobile money — all over a REST API.
Every request requires your partner API key as a Bearer token in the Authorization header. Keys are environment-scoped.
curl -X POST https://www.ntzs.co.tz/api/v1/users \
-H "Authorization: Bearer ntzs_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"externalId":"user_1","email":"user@example.com"}'ntzs_live_, test keys with ntzs_test_. Generate or rotate your key from the partner dashboard.Register a user and provision an on-chain wallet in a single call. Wallets are deterministically derived from your partner seed — no blockchain transaction required.
const res = await fetch('https://www.ntzs.co.tz/api/v1/users', {
method: 'POST',
headers: {
'Authorization': 'Bearer ntzs_live_xxxxxxxxxxxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
externalId: 'your-internal-user-id', // required — your own system's user ID
email: 'user@example.com', // required
name: 'Jane Doe', // optional
phone: '255712345678', // optional, Tanzanian format
}),
}){
"id": "14e17d04-ec7f-4d99-91a3-dfbaca19fba1",
"externalId": "your-internal-user-id",
"email": "user@example.com",
"name": "Jane Doe",
"phone": "255712345678",
"walletAddress": "0x531B87EfdEBD19bfd05700DF6218d4786Cf2201C",
"balance": 0
}id field. This is the nTZS user ID you will pass as userId in all subsequent requests (deposits, transfers, withdrawals). It is different from your own externalId.externalId returns the existing user. Safe to call on every login.Fetch a user's on-chain nTZS balance alongside their profile. The balance is read live from Base mainnet at request time.
const res = await fetch(
'https://www.ntzs.co.tz/api/v1/users/14e17d04-ec7f-4d99-91a3-dfbaca19fba1',
{ headers: { 'Authorization': 'Bearer ntzs_live_xxxxxxxxxxxx' } }
)
const user = await res.json()
// {
// id: "14e17d04-ec7f-4d99-91a3-dfbaca19fba1",
// externalId: "your-internal-user-id",
// email: "user@example.com",
// phone: "255712345678",
// walletAddress: "0x531B87EfdEBD19bfd05700DF6218d4786Cf2201C",
// balanceTzs: 25000, // nTZS balance (18 decimals, integer TZS units)
// balanceUsdc: 6.50 // USDC balance (6 decimals, float)
// }Initiate a payment in Tanzanian Shillings. On success, nTZS is minted 1:1 to the user's wallet. Supports mobile money and card payments.
id returned from POST /api/v1/users — not your own externalId.const res = await fetch('https://www.ntzs.co.tz/api/v1/deposits', {
method: 'POST',
headers: {
'Authorization': 'Bearer ntzs_live_xxxxxxxxxxxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId: '14e17d04-ec7f-4d99-91a3-dfbaca19fba1', // id from POST /api/v1/users
amountTzs: 10000, // minimum 500 TZS
paymentMethod: 'mobile_money', // default
phoneNumber: '255712345678', // required for mobile_money — use phoneNumber, not phone
}),
})
// { id, status: "submitted", amountTzs: 10000,
// paymentMethod: "mobile_money",
// instructions: "Check your phone for the mobile money payment prompt" }body: JSON.stringify({
userId: user.id,
amountTzs: 10000,
paymentMethod: 'card',
redirectUrl: 'https://yourapp.com/payment/success', // required, must be HTTPS
cancelUrl: 'https://yourapp.com/payment/cancel', // required, must be HTTPS
})
// { id, status: "submitted", amountTzs: 10000,
// paymentMethod: "card",
// paymentUrl: "https://pay.snippe.sh/c/..." }
// → redirect your user to paymentUrl to complete card payment// Payment-collection mode: mint nTZS directly to your platform treasury
// instead of the user's individual wallet. Useful for marketplaces and
// escrow flows where you collect funds before distributing them.
body: JSON.stringify({
userId: user.id, // the payer, for tracking
amountTzs: 50000,
paymentMethod: 'mobile_money',
phoneNumber: '255712345678',
collectToTreasury: true, // mint to partner treasury wallet
})Move nTZS or USDC between platform users or to any external wallet address. Settlement is on-chain and synchronous — the API responds only after the transaction is confirmed.
const res = await fetch('https://www.ntzs.co.tz/api/v1/transfers', {
method: 'POST',
headers: {
'Authorization': 'Bearer ntzs_live_xxxxxxxxxxxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
fromUserId: 'uuid-of-sender',
toUserId: 'uuid-of-recipient', // nTZS user on your platform
amountTzs: 5000,
metadata: { orderId: 'ord_123', note: 'Payment for order' }, // optional
}),
})
const transfer = await res.json()
// {
// id: "uuid...",
// status: "completed",
// txHash: "0xabc...",
// amountTzs: 5000,
// recipientAmountTzs: 4975, // after platform fee
// feeAmountTzs: 25,
// feeTxHash: "0xdef...", // fee tx to your treasury, if fee > 0
// toAddress: "0x531B..." // resolved destination wallet
// }// Send nTZS to ANY wallet address — no recipient user required.
// Use toAddress instead of toUserId.
const res = await fetch('https://www.ntzs.co.tz/api/v1/transfers', {
method: 'POST',
headers: {
'Authorization': 'Bearer ntzs_live_xxxxxxxxxxxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
fromUserId: 'uuid-of-sender',
toAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18', // any valid EVM address
amountTzs: 10000,
}),
})
const transfer = await res.json()
// {
// id: "uuid...",
// status: "completed",
// txHash: "0xabc...",
// amountTzs: 10000,
// recipientAmountTzs: 9950,
// feeAmountTzs: 50,
// toAddress: "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18"
// }// Same endpoint — add token: 'USDC' and use the token-agnostic amount field.
// USDC uses 6 decimals — fractional amounts are supported.
const res = await fetch('https://www.ntzs.co.tz/api/v1/transfers', {
method: 'POST',
headers: {
'Authorization': 'Bearer ntzs_live_xxxxxxxxxxxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
fromUserId: 'uuid-of-sender',
toAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18',
token: 'USDC',
amount: 12.5, // 12.5 USDC
}),
})
const transfer = await res.json()
// {
// id: "uuid...",
// status: "completed",
// txHash: "0xabc...",
// token: "usdc",
// amount: 12.5,
// recipientAmount: 12.4375, // after platform fee
// feeAmount: 0.0625,
// feeTxHash: "0xdef...",
// toAddress: "0x742d35..."
// // Legacy amountTzs / recipientAmountTzs / feeAmountTzs are omitted for non-nTZS tokens.
// }toUserId sends to a platform user's wallet. toAddress sends to any Ethereum-compatible address on Base. Both fields cannot be set at the same time.Burns nTZS tokens on-chain and sends TZS to the user's mobile money number. Supports all major Tanzanian mobile networks. The burn and payout happen automatically.
const res = await fetch('https://www.ntzs.co.tz/api/v1/withdrawals', {
method: 'POST',
headers: {
'Authorization': 'Bearer ntzs_live_xxxxxxxxxxxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId: user.id,
amountTzs: 10000, // minimum 5,000 TZS
phoneNumber: '255712345678', // mobile money recipient (Vodacom, Airtel, Tigo, Halotel, TTCL, Yass)
}),
})
const withdrawal = await res.json()
// Small amounts (< 1,000,000 TZS):
// { id, status: "burned", amountTzs: 10000,
// message: "Withdrawal processed successfully." }
//
// Large amounts (>= 1,000,000 TZS):
// { id, status: "requested", amountTzs: 150000,
// message: "Withdrawal requires admin approval for amounts >= 1,000,000 TZS." }Public endpoint — no API key required. Use this to show users a live quote before they confirm. Rates are valid for ~30 seconds.
expectedOutput and minOutput → call POST /api/v1/swap within the expiresAt window.// Same-chain: USDT → nTZS on Base
const res = await fetch(
'https://www.ntzs.co.tz/api/v1/swap/rate?from=USDT&to=NTZS&amount=100'
)
const rate = await res.json()
// {
// from: "USDT", to: "NTZS", amount: 100,
// midRate: 3750, // market reference rate
// rate: 3744.375, // effective rate after LP spread
// expectedOutput: 374437.5,
// minOutput: 370993.1, // with 1% slippage guard
// expiresAt: "2026-04-27T10:00:30.000Z",
// lowLiquidity: false
// }
// Cross-chain: USDT on BNB → nTZS on Base
const crossRate = await fetch(
'https://www.ntzs.co.tz/api/v1/swap/rate' +
'?from=USDT&to=NTZS&fromChain=bnb&toChain=base&amount=50'
)true, the pool may not fill the full amount. Warn the user and consider reducing the swap size.SLIPPAGE_EXCEEDED.Execute a swap for any WaaS user. Supports nTZS, USDC, and USDT on Base — plus cross-chain USDT swaps via BNB Smart Chain. Streams status in real time over SSE.
const res = await fetch('https://www.ntzs.co.tz/api/v1/swap', {
method: 'POST',
headers: {
'Authorization': 'Bearer ntzs_live_xxxxxxxxxxxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId: user.id,
fromToken: 'USDT', // 'NTZS' | 'USDC' | 'USDT'
toToken: 'NTZS',
amount: 100,
slippageBps: 100, // optional, default 100 (1%)
}),
})
const reader = res.body!.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
for (const line of decoder.decode(value).split('\n')) {
if (!line.startsWith('data: ')) continue
const event = JSON.parse(line.slice(6))
// CHECKING → balance check
// SENDING → user → solver transfer (txHash)
// FILLING → solver → user transfer (txHash)
// FILLED → complete (txHash)
// FAILED → event.error for code
if (event.status === 'FILLED' || event.status === 'FAILED') break
}
}body: JSON.stringify({
userId: user.id,
fromToken: 'USDT',
toToken: 'NTZS',
fromChain: 'bnb', // USDT sent from BNB Smart Chain
toChain: 'base', // nTZS received on Base
amount: 50,
slippageBps: 150,
})curl -N -X POST https://www.ntzs.co.tz/api/v1/swap \
-H "Authorization: Bearer ntzs_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"userId":"uuid...","fromToken":"USDT","toToken":"NTZS","amount":100}'
# data: {"status":"CHECKING","message":"Checking balance..."}
# data: {"status":"SENDING","message":"Sending 100 USDT to liquidity pool...","txHash":"0x..."}
# data: {"status":"FILLING","message":"Sending nTZS to your wallet...","txHash":"0x..."}
# data: {"status":"FILLED","message":"Swap complete!","txHash":"0x..."}slippageBps between rate fetch and execution, you receive FAILED / SLIPPAGE_EXCEEDED. Re-fetch the rate and let the user re-confirm before retrying.GET /api/v1/users/:id after FILLED — returns live on-chain balances for nTZS, USDC, and USDT with no caching.Receive real-time POST notifications to your server when payment events complete. Configure your endpoint and signing secret in the partner dashboard.
import crypto from 'crypto'
app.post('/webhooks/ntzs', express.raw({ type: 'application/json' }), (req, res) => {
// Verify signature
const sig = req.headers['x-ntzs-signature'] as string
const expected = crypto
.createHmac('sha256', process.env.NTZS_WEBHOOK_SECRET!)
.update(req.body)
.digest('hex')
if (sig !== expected) {
return res.status(400).send('Invalid signature')
}
const event = JSON.parse(req.body.toString())
switch (event.type) {
case 'deposit.completed':
// event.data: { depositId, userId, amountTzs, walletAddress, txHash }
await creditUserAccount(event.data.userId, event.data.amountTzs)
break
case 'transfer.completed':
// event.data: { transferId, fromUserId, toUserId, amountTzs, txHash }
break
case 'withdrawal.completed':
// event.data: { withdrawalId, userId, amountTzs, phoneNumber }
break
}
res.status(200).json({ received: true })
})All errors return a consistent JSON body. Match on the error field for programmatic handling.
// HTTP 4xx/5xx response body:
{
"error": "insufficient_balance", // machine-readable code
"message": "Sender has insufficient nTZS balance",
"details": {
"available": 3200,
"requested": 5000,
"shortfall": 1800
}
}| Error code | Status | Meaning |
|---|---|---|
| missing_required_fields | 400 | A required body field is absent |
| invalid_amount | 400 | Amount is zero, negative, or below minimum |
| invalid_transfer | 400 | fromUserId equals toUserId |
| wallet_not_provisioned | 400 | Wallet address is still being derived |
| insufficient_balance | 400 | Sender does not have enough nTZS |
| user_not_found | 404 | userId not found under your partner account |
| unauthorized | 401 | Missing or invalid API key |
| relayer_unavailable | 503 | Gas relay temporarily offline — retry shortly |
| blockchain_error | 500 | On-chain transaction failed — see details.technicalError |
| network_error | 500 | RPC connection timed out — retry |