TeePayDocs
SDKs

JavaScript / TypeScript SDK

Accept payments, create hosted checkouts, send payouts and verify webhooks from Node.js with @teepay/sdk.

Package
@teepay/sdk
Install
npm install @teepay/sdk
Runtime
Node.js 18 or newer (global fetch), zero dependencies
Modules
ESM and CommonJS, TypeScript types included

Install

npm install @teepay/sdk

Create a client

import TeePay from '@teepay/sdk';
// CommonJS: const { TeePay } = require('@teepay/sdk');

const teepay = new TeePay(process.env.TEEPAY_API_KEY); // tp_test_… or tp_live_…

console.log(teepay.environment); // "sandbox" or "live"
const caps = await teepay.capabilities();
console.log(caps.features.collection_create, caps.supported_countries);

new TeePay() with no argument reads TEEPAY_API_KEY. Options:

OptionDefault
timeout30000Milliseconds per attempt
maxRetries2Retries for safe requests
baseUrlhttps://api.teepay.me
apiVersionnot sentPins the TeePay-Version header, e.g. '2026-10-18'
fetchglobal fetchCustom implementation
const teepay = new TeePay(process.env.TEEPAY_API_KEY, { timeout: 15_000, maxRetries: 3 });

Mobile money push

Sends a USSD prompt to the customer's phone. See Mobile money push.

const payment = await teepay.collections.create(
  {
    money: { amount_minor: '500000', currency: 'TZS' }, // TZS 5,000.00
    customer: { country: 'TZ', phone: '+255712345678', name: 'Asha Juma' },
    description: 'Order 1001',
  },
  { idempotencyKey: 'order-1001' },
);

// 202 Accepted: the prompt was sent, the customer has not paid yet.
console.log(payment.public_reference, payment.status); // "TP8K2M4QZ1" "pending"

const latest = await teepay.collections.retrieve(payment.public_reference!);
if (latest.status === 'succeeded') {
  // fulfil the order
} else if (latest.status === 'failed') {
  // latest.status_detail is "cancelled" or "expired" when known
}

Hosted checkout session

See Checkout sessions.

const session = await teepay.checkoutSessions.create(
  {
    title: 'Order 1001',
    description: '2 x Coffee bag',
    amount_minor: '500000',
    currency: 'TZS',
    reference: 'order-1001',
    metadata: { cart: 'c-77' }, // keys must be lowercase
  },
  { idempotencyKey: 'checkout-order-1001' },
);

res.redirect(303, session.checkout_url!);

// Later, on your server:
const current = await teepay.checkoutSessions.retrieve(session.id);
if (current.status === 'paid') console.log(current.payment?.reference);

status becomes paid only after TeePay verified the payment. Never fulfil from a browser redirect alone.

Control Numbers and payouts

const number = await teepay.controlNumbers.create(
  { kind: 'bill', customer: { name: 'Asha Juma', phone: '0712345678' }, description: 'School fees, term 1', amount_minor: '5000000' },
  { idempotencyKey: 'invoice-2026-0077' },
);
if (number.lastResponse.status === 202) {
  // Not issued yet: call create again later with the SAME idempotencyKey.
}

const quote = await teepay.disbursements.quote({
  money: { amount: 2500000, currency: 'TZS' }, // payout quotes take an integer
  destination: { channel: 'mobile', name: 'Juma Ali', phone: '255712345678' },
  narration: 'Supplier payment October',
});
const payout = await teepay.disbursements.create({ quote_id: quote.id, idempotency_key: 'payout-2026-10-0001' });

Lists

list() returns one page; listAll() is an async iterator that follows next_cursor.

const page = await teepay.collections.list({ limit: 50, status: 'succeeded' });
console.log(page.items.length, page.next_cursor);

for await (const payment of teepay.collections.listAll({ status: 'succeeded' })) {
  console.log(payment.public_reference);
}

Control Number lists put their rows in page.data; the others use page.items.

Verify webhooks

Use the raw body. With Express, mount express.raw on the webhook route only.

import express from 'express';
import { verifyWebhook, WebhookVerificationError } from '@teepay/sdk';

const app = express();

app.post('/webhooks/teepay', express.raw({ type: 'application/json' }), (req, res) => {
  let event;
  try {
    event = verifyWebhook(req.body, req.headers, process.env.TEEPAY_WEBHOOK_SECRET!);
  } catch (error) {
    if (error instanceof WebhookVerificationError) return res.sendStatus(400);
    throw error;
  }
  if (event.event === 'payment.completed') {
    // Find the order by event.data.reference; process each event.id once.
  }
  res.sendStatus(200);
});

verifyWebhook(payload, headers, secret, { tolerance: 300 }) accepts a string, Buffer or Uint8Array and Node, Express or Fetch headers. teepay.webhooks.verify(...) is the same function; teepay.webhooks.isValid(...) returns a boolean, and generateTestHeaders(body, secret) signs a payload for your tests.

Errors

import { ApiError, AccountRestrictedError, ValidationError, RateLimitError, ConnectionError } from '@teepay/sdk';

try {
  await teepay.collections.create(params, { idempotencyKey: 'order-1001' });
} catch (error) {
  if (error instanceof AccountRestrictedError) {
    // 403 account_restricted: contact TeePay support
  } else if (error instanceof ValidationError) {
    console.log(error.code); // invalid_request, unsupported_market, invalid_amount, ...
  } else if (error instanceof RateLimitError) {
    console.log('retry after', error.retryAfter, 'seconds');
  } else if (error instanceof ConnectionError) {
    // No response: retry with the SAME key or read the payment.
  } else if (error instanceof ApiError) {
    console.log(error.status, error.code, error.requestId);
  }
}
ClassWhen
AuthenticationError401 invalid_credential
PermissionError403 scope_denied, access_denied, developer_access_denied
AccountRestrictedError403 account_restricted (also a PermissionError)
NotFoundError404 not_found
ConflictError409 idempotency_conflict, expired payout quote
ValidationError422 codes such as invalid_request and unsupported_market
RateLimitError429 rate_limited, with retryAfter
ApiErrorAny other HTTP error; base class of all of the above
ConnectionError, TimeoutErrorNo HTTP response
WebhookVerificationErrorBad webhook signature or timestamp

Idempotency and retries

collections.create, checkoutSessions.create and controlNumbers.create send an Idempotency-Key header; disbursements.create sends idempotency_key in the body. Without one, the SDK generates a key and reuses it for its own retries. Derive keys from your own records (order-1001) so a retry after a crash cannot create a second payment. See Idempotency.

Retries happen only for GET requests and keyed creates, on connection errors, timeouts, 408, 429, 500, 502, 503 and 504, using Retry-After when present. 4xx errors are never retried.

Response metadata

Every result has a non-enumerable lastResponse:

const session = await teepay.checkoutSessions.create(params, { idempotencyKey: 'checkout-order-1001' });
session.lastResponse.requestId;          // quote to support
session.lastResponse.idempotentReplayed; // true when the same key returned an earlier session
session.lastResponse.rateLimit;          // { limit, remaining, reset } or null

Keep your key on the server

The SDK throws if it detects a browser. Your frontend should call your own server, which creates the checkout session and returns checkout_url.

On this page