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 (< 100,000 TZS):
// { id, status: "burned", amountTzs: 10000,
// message: "Withdrawal processed successfully." }
//
// Large amounts (>= 100,000 TZS):
// { id, status: "requested", amountTzs: 150000,
// message: "Withdrawal requires admin approval for amounts >= 100,000 TZS." }Let users swap between nTZS and USDC on Base. The swap settles directly against the LP pool and streams real-time status 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: 'USDC', // 'USDC' or 'NTZS'
toToken: 'NTZS',
amount: 5, // in fromToken units
slippageBps: 100, // optional, default 100 (1%)
}),
})
// Response is text/event-stream — read with EventSource or manually:
const reader = res.body!.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const lines = decoder.decode(value).split('\n')
for (const line of lines) {
if (!line.startsWith('data: ')) continue
const update = JSON.parse(line.slice(6))
console.log(update.status, update.message, update.txHash)
// CHECKING → "Checking balance..."
// SENDING → "Sending 5 USDC to liquidity pool..." txHash
// FILLING → "Sending nTZS to your wallet..." txHash
// FILLED → "Swap complete!" txHash (final)
// FAILED → error message
}
}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":"USDC","toToken":"NTZS","amount":5}'
# data: {"status":"CHECKING","message":"Checking balance..."}
# data: {"status":"SENDING","message":"Sending 5 USDC to liquidity pool...","txHash":"0x..."}
# data: {"status":"FILLING","message":"Sending nTZS to your wallet...","txHash":"0x..."}
# data: {"status":"FILLED","message":"Swap complete!","txHash":"0x..."}GET /api/v1/users/:id. A nTZS→USDC swap increases balanceUsdc and decreases balanceTzs. A USDC→nTZS swap does the reverse. Both fields reflect live on-chain state — 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 |