TeePayDocs
Payments

Countries and currencies

How TeePay markets work, how to discover them at runtime, and how amounts, phone numbers and currency conversion behave in each country.

TeePay is built to collect in more than one country. A market is a country together with its local currency, such as Tanzania with TZS. Every payment names its market explicitly, and your key reports which markets it can use. Build against that model and a new country works in your integration the day TeePay enables it, with no code change.

Never hard-code a country

Read supported_countries and supported_currencies from GET /v1/capabilities, and store the currency next to every amount. Do not assume Tanzania, TZS or two decimal places anywhere.

Markets

Marketcustomer.countrymoney.currencyMinor units"amount_minor" exampleStatus
TanzaniaTZTZS2"500000" = TZS 5,000.00Requires approval
KenyaKEKES2"50000" = KES 500.00Not available yet
UgandaUGUGX0"5000" = UGX 5,000Not available yet

A market becomes available only after TeePay has verified a payment route there and enabled it for your account. The table tells you what to expect; capabilities tells you what your key can do right now. Control Numbers are a Tanzanian payment method and stay TZS only. Payouts are currently sent in TZS.

Discover markets at runtime

curl https://api.teepay.me/v1/capabilities \
  -H "Authorization: Bearer $TEEPAY_SECRET_KEY"
{
  "features": { "collection_create": true, "...": "..." },
  "supported_countries": ["TZ"],
  "supported_currencies": ["TZS"]
}

Both lists are sorted and contain only markets your key can collect in now: TeePay has a working route there, your key has collection:create, and your account's limits allow it. They are empty when collection_create is false.

markets.js
// Cache for a few minutes; re-read after an unsupported_market error.
export async function canCollect(country, currency) {
  const response = await fetch('https://api.teepay.me/v1/capabilities', {
    headers: { Authorization: `Bearer ${process.env.TEEPAY_SECRET_KEY}` },
  });
  const caps = await response.json();
  return caps.features.collection_create
    && caps.supported_countries.includes(country)
    && caps.supported_currencies.includes(currency);
}

Use it to decide which payment options to show at checkout, for example to offer mobile money only to customers in a market your account can serve.

Collect in a market

Every market uses the same mobile money push endpoint. Only the market fields change:

POST /v1/collections
{
  "money": { "amount_minor": "500000", "currency": "TZS" },
  "customer": {
    "country": "TZ",
    "phone": "+255712345678",
    "name": "Asha Juma",
    "email": "asha@example.com"
  },
  "description": "Order 1001"
}

If the pair is not enabled for your key, nothing is sent to the customer and the request fails with unsupported_market. Checkout sessions take the same ISO 4217 currency and fail with unsupported_currency when it is not enabled.

Amounts and minor units

Every amount is an integer in the currency's minor unit, as defined by ISO 4217. Requests send it as a string, responses return it as a number, and the currency always travels with it.

money.js
// ISO 4217 exponents. Add a currency here when you start selling in it.
const EXPONENT = { TZS: 2, KES: 2, UGX: 0 };

export function toMinor(major, currency) {
  const exponent = EXPONENT[currency];
  if (exponent === undefined) throw new Error('Unsupported currency ' + currency);
  // Work from a decimal string, never floating-point arithmetic, for real money.
  const [whole, fraction = ''] = String(major).split('.');
  if (fraction.length > exponent) throw new Error('Too many decimal places for ' + currency);
  return BigInt(whole + fraction.padEnd(exponent, '0')).toString();
}

export function format(minor, currency, locale = 'en') {
  const exponent = EXPONENT[currency];
  return new Intl.NumberFormat(locale, { style: 'currency', currency, minimumFractionDigits: exponent, maximumFractionDigits: exponent })
    .format(Number(minor) / 10 ** exponent);
}

Each market has its own minimum, maximum and amount step, and your account has per-transaction and daily limits. An amount outside the market's range fails with invalid_amount; one above your available limit fails with collection_limit_exceeded. In both cases nothing is charged.

Phone numbers

Send the payer's mobile money number in international format with the country code, such as +255712345678 (Tanzania), +254712345678 (Kenya) or +256712345678 (Uganda). The number must belong to a mobile money wallet in customer.country.

Currency conversion

Not available yet

Today every payment is collected and settled in the same currency, so there is no conversion fee.

When TeePay enables conversion, for example collecting KES and settling to a TZS balance, it will work like this:

  • The fee is separate. Conversion is charged as its own fee, never hidden inside the exchange rate.
  • It is charged in your settlement currency and rounded up once to that currency's smallest unit.
  • The price is fixed when the payment happens. Later price changes never change a past payment.
  • Same currency means no conversion fee, even when the payer is in another country.

The response fields that describe conversion will be added to the API and to this page before the feature is switched on. Following versioning rules, they are additive: integrations that tolerate unknown fields keep working unchanged.

Errors

HTTPCodeMeaningWhat to do
422unsupported_marketcustomer.country and money.currency are not enabled for your key.Re-read capabilities and offer another payment method.
422invalid_amountBelow the market's minimum, above its maximum or not a valid step.Use an amount the market accepts.
422collection_limit_exceededAbove your available transaction or daily limit.Lower the amount or wait.
422unsupported_currencyCheckout session currency not enabled for your account.Use an enabled currency.

Checklist

  • Store currency next to every amount in your database.
  • Convert with the currency's exponent; never assume two decimal places.
  • Read capabilities at start-up and cache them for a few minutes.
  • Send customer.country and an international phone number on every collection.
  • Treat unsupported_market as "not available here", not as a bug in your request.

On this page