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/sdkCreate 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:
| Option | Default | |
|---|---|---|
timeout | 30000 | Milliseconds per attempt |
maxRetries | 2 | Retries for safe requests |
baseUrl | https://api.teepay.me | |
apiVersion | not sent | Pins the TeePay-Version header, e.g. '2026-10-18' |
fetch | global fetch | Custom 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);
}
}| Class | When |
|---|---|
AuthenticationError | 401 invalid_credential |
PermissionError | 403 scope_denied, access_denied, developer_access_denied |
AccountRestrictedError | 403 account_restricted (also a PermissionError) |
NotFoundError | 404 not_found |
ConflictError | 409 idempotency_conflict, expired payout quote |
ValidationError | 422 codes such as invalid_request and unsupported_market |
RateLimitError | 429 rate_limited, with retryAfter |
ApiError | Any other HTTP error; base class of all of the above |
ConnectionError, TimeoutError | No HTTP response |
WebhookVerificationError | Bad 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 nullKeep 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.