Developer Documentation

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.

Base URL
https://www.ntzs.co.tz
Network
Base mainnet
Token
nTZS (ERC-20, 18 decimals)
Step 1

Authentication

Every request requires your partner API key as a Bearer token in the Authorization header. Keys are environment-scoped.

curl
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"}'
Key format: Production keys start with ntzs_live_, test keys with ntzs_test_. Generate or rotate your key from the partner dashboard.
Security: Never expose your API key in client-side or mobile code. All nTZS API calls must originate from your backend server.
Step 2

Create users

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.

POST /api/v1/users — request
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
  }),
})
201 response
{
  "id": "14e17d04-ec7f-4d99-91a3-dfbaca19fba1",
  "externalId": "your-internal-user-id",
  "email": "user@example.com",
  "name": "Jane Doe",
  "phone": "255712345678",
  "walletAddress": "0x531B87EfdEBD19bfd05700DF6218d4786Cf2201C",
  "balance": 0
}
Store the 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.
Idempotent: Calling with the same externalId returns the existing user. Safe to call on every login.
Gas pre-funded: New wallets are automatically topped up with a small ETH amount for gas. You do not need to fund wallets yourself.
Step 3

Get user profile & balance

Fetch a user's on-chain nTZS balance alongside their profile. The balance is read live from Base mainnet at request time.

GET /api/v1/users/:id
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)
// }
balanceTzs — live nTZS balance read from the nTZS contract on Base mainnet. Increases on deposit, decreases on withdrawal or nTZSUSDC swap.
balanceUsdc — live USDC balance in the same wallet. Accumulates when the user swaps nTZSUSDC. Both balances are fetched in parallel in a single API call.
Both fields are read directly from Base mainnet at request time — no caching. Always use this endpoint before initiating a transfer or withdrawal to confirm the user has sufficient funds.
Step 4

Accept deposits (On-Ramp)

Initiate a payment in Tanzanian Shillings. On success, nTZS is minted 1:1 to the user's wallet. Supports mobile money and card payments.

userId must be the id returned from POST /api/v1/users — not your own externalId.
POST /api/v1/deposits — mobile money
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" }
POST /api/v1/deposits — card
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
POST /api/v1/deposits — collect to treasury
// 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
})
Minimum
500 TZS
Mobile providers
Vodacom (M-Pesa), Airtel (Airtel Money), Tigo (Tigo Pesa), Halotel (HaloPesa), TTCL (TTCL Pesa), Yass
Settlement
Real-time on Base mainnet after payment confirmation
Step 5

Transfers

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.

POST /api/v1/transfers — user to user
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
// }
POST /api/v1/transfers — send to external address
// 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"
// }
POST /api/v1/transfers — USDC transfer
// 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 vs toAddress: Provide exactly one. 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.
Platform fee: Configure your fee percentage and treasury wallet address in the dashboard. The fee is deducted from the sender and sent to your treasury in the same atomic operation.
Requirements: The sender must belong to your platform, their wallet must be provisioned, and they must have sufficient balance. For user-to-user transfers, the recipient must also belong to your platform. Gas is auto-managed — if the sender wallet is low on ETH, the relayer tops it up before sending.
Step 6

Cash out to mobile money (Off-Ramp)

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.

POST /api/v1/withdrawals
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." }
Minimum
5,000 TZS
Large withdrawal threshold
>= 100,000 TZS requires admin approval and may take up to 1 business day
Advanced

Swap nTZS / USDC

Let users swap between nTZS and USDC on Base. The swap settles directly against the LP pool and streams real-time status over SSE.

POST /api/v1/swap — SSE stream
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 (raw SSE)
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..."}
Supported pairs
nTZS / USDC (both directions)
Settlement
Two on-chain ERC-20 transfers on Base, ~5–10 seconds
Gas
Auto-managed. User wallet is pre-funded via the relayer if needed.
Balance tracking after a swap: Once a swap settles, the user's stablecoin holdings are immediately visible via GET /api/v1/users/:id. A nTZSUSDC swap increases balanceUsdc and decreases balanceTzs. A USDCnTZS swap does the reverse. Both fields reflect live on-chain state — no caching.
Events

Webhooks

Receive real-time POST notifications to your server when payment events complete. Configure your endpoint and signing secret in the partner dashboard.

webhook-handler.ts (Express)
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 })
})
Configure your webhook URL and secret in the partner dashboard under Settings. Events are signed with HMAC-SHA256 — always verify the signature before processing.
Reference

Error reference

All errors return a consistent JSON body. Match on the error field for programmatic handling.

Error response shape
// 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 codeStatusMeaning
missing_required_fields400A required body field is absent
invalid_amount400Amount is zero, negative, or below minimum
invalid_transfer400fromUserId equals toUserId
wallet_not_provisioned400Wallet address is still being derived
insufficient_balance400Sender does not have enough nTZS
user_not_found404userId not found under your partner account
unauthorized401Missing or invalid API key
relayer_unavailable503Gas relay temporarily offline — retry shortly
blockchain_error500On-chain transaction failed — see details.technicalError
network_error500RPC connection timed out — retry