tbc-rando-sdk
v0.1.0
Published
TypeScript SDK for the TBC Bank Checkout (TPAY) API — card payments, installments and subscriptions for Node.js and Next.js
Maintainers
Readme
tbc-rando-sdk
TypeScript SDK for the TBC Bank Checkout (TPAY) API — card payments, installments and subscriptions (recurring payments) for Node.js and Next.js.
- Zero runtime dependencies (uses the built-in
fetch, Node ≥ 18.17) - Automatic access-token caching and refresh (single token request even under concurrent load, transparent retry on 401)
- Typed request/response models with client-side validation before anything hits the network
- Handles TBC's wire-format quirks for you (
returnurllowercase, PascalCaseinstallmentProducts) - Webhook/callback helper that plugs straight into a Next.js App Router route
Install
npm install tbc-rando-sdkSetup
Credentials come from developers.tbcbank.ge (app apikey) and your TBC E-Commerce merchant dashboard (client_id / client_secret):
import { TbcCheckoutClient } from 'tbc-rando-sdk';
const tbc = new TbcCheckoutClient({
apiKey: process.env.TBC_API_KEY!,
clientId: process.env.TBC_CLIENT_ID!,
clientSecret: process.env.TBC_CLIENT_SECRET!,
});Create the client once (e.g. in a module) and reuse it — the ~24h access token is cached inside the instance. Server-side only: never expose these credentials to the browser. In Next.js use it in route handlers, server actions or server components.
1. Standard payment
import { getRedirectUrl } from 'tbc-rando-sdk';
const payment = await tbc.payments.create({
amount: { currency: 'GEL', total: 49.9 },
returnUrl: 'https://shop.example.ge/checkout/return',
callbackUrl: 'https://shop.example.ge/api/tbc/callback',
merchantPaymentId: 'ORDER-1042',
description: 'Order #1042',
language: 'EN',
});
// Send the customer to the hosted TBC checkout page:
const checkoutUrl = getRedirectUrl(payment);Then check the outcome (from your callback handler or by polling):
import { isFinalPaymentStatus, PaymentStatus } from 'tbc-rando-sdk';
const details = await tbc.payments.retrieve(payment.payId);
if (details.status === PaymentStatus.Succeeded) {
// fulfill the order
}
isFinalPaymentStatus(details.status); // stop polling when trueRefunds / cancellations and pre-authorizations:
await tbc.payments.cancel(payId); // full refund
await tbc.payments.cancel(payId, { amount: 10.5 }); // partial refund
// preAuth: true on create blocks the amount for up to 30 days…
await tbc.payments.completePreAuth(payId, 45.0); // …then capture (≤ blocked amount)2. Installments
Installments are checkout payments restricted to payment method 8. Your merchant account needs an installment campaignId/merchantKey configured in the TBC back office. The customer picks the terms on TBC's page.
const payment = await tbc.installments.create({
returnUrl: 'https://shop.example.ge/checkout/return',
callbackUrl: 'https://shop.example.ge/api/tbc/callback',
merchantPaymentId: 'ORDER-1043',
products: [
{ name: 'Laptop', price: 2199, quantity: 1 },
{ name: 'Mouse', price: 89.5, quantity: 2 },
],
// total is computed as sum(price × quantity) = 2378.00; currency defaults to GEL
});
const checkoutUrl = getRedirectUrl(payment);The SDK validates product lines, computes the total with proper money rounding, and serializes products to the PascalCase wire format TBC expects ({ Name, Price, Quantity }).
3. Subscriptions (recurring payments)
TBC subscriptions are built on saved cards (card saving must be enabled for your merchant by TBC):
Step 1 — the customer pays once and the card is saved:
const first = await tbc.subscriptions.start({
amount: { currency: 'GEL', total: 9.99 },
returnUrl: 'https://app.example.ge/subscribe/return',
callbackUrl: 'https://app.example.ge/api/tbc/callback',
description: 'Monthly plan',
saveCardToDate: '1230', // optional MMYY limit
});
// redirect the customer to getRedirectUrl(first)Step 2 — after the payment succeeds, persist the card token:
const details = await tbc.payments.retrieve(first.payId);
const recId = details.recurringCard?.recId; // store this against the subscriberStep 3 — bill on your own schedule (cron, queue, etc.), no customer interaction:
const charge = await tbc.subscriptions.charge({
recId,
amount: 9.99,
currency: 'GEL',
initiator: 'merchant', // merchant-initiated (unattended) billing
merchantPaymentId: 'SUB-1042-2026-08',
});
if (charge.status === 'Succeeded') {
// extend the subscription
}Cancel the subscription (deletes the saved card):
await tbc.subscriptions.cancel(recId);Callbacks (webhooks) in Next.js
TBC POSTs a PaymentId to your callbackUrl when a payment reaches a final status. The body carries no status — the handler fetches the authoritative payment details from the API before invoking your listener, so forged callbacks can't inject a fake status.
// app/api/tbc/callback/route.ts
import { tbc } from '@/lib/tbc'; // your shared TbcCheckoutClient instance
export const POST = tbc.webhooks.handler(async (payment) => {
switch (payment.status) {
case 'Succeeded':
await fulfillOrder(payment.payId);
break;
case 'Failed':
case 'Expired':
await markOrderFailed(payment.payId);
break;
}
});Lower-level pieces if you're not on the App Router:
import { parseCallbackBody, TBC_CALLBACK_IPS } from 'tbc-rando-sdk';
// pages/api/tbc-callback.ts
export default async function handler(req, res) {
const { payment } = await tbc.webhooks.resolveCallback(req.body);
// ...handle payment.status
res.status(200).send('OK');
}TBC_CALLBACK_IPS lists the four IPs TBC sends callbacks from, for firewall whitelisting; the route helper can also enforce them via { verifySourceIp: true } (only when X-Forwarded-For is set by your own proxy).
Error handling
import { TbcApiError, TbcAuthError, TbcValidationError } from 'tbc-rando-sdk';
try {
await tbc.subscriptions.charge({ recId, amount: 9.99, currency: 'GEL', initiator: 'merchant' });
} catch (error) {
if (error instanceof TbcApiError) {
error.status; // HTTP status
error.resultCode; // e.g. 'decline_not_sufficient_funds', 'decline_expired_card'
error.systemCode; // TBC problem code, e.g. 'tpay.400.012'
}
}TbcValidationError— thrown client-side before any request is sentTbcAuthError— HTTP 401 (badapikeyor token; the SDK already retried once with a fresh token)TbcApiError— any other non-2xx API response, withresultCodefor business declines (reference)
Decline codes are available as constants:
import { ResultCode } from 'tbc-rando-sdk';
if (error instanceof TbcApiError && error.resultCode === ResultCode.DeclineNotSufficientFunds) {
// ask the customer for another card
}Constants
PaymentMethod, PaymentStatus, ResultCode, Currency and Language are exported as enum-style as const objects (each name is also the matching TypeScript type). Unlike real TS enums, plain strings stay assignable — status === 'Succeeded' and status === PaymentStatus.Succeeded both type-check, and the objects tree-shake cleanly.
Payment methods
Restrict the checkout page via methods:
import { PaymentMethod } from 'tbc-rando-sdk';
await tbc.payments.create({
amount: { currency: 'GEL', total: 20 },
returnUrl: '…',
methods: [PaymentMethod.Card, PaymentMethod.ApplePay, PaymentMethod.GooglePay],
});| Constant | ID | Notes |
| --- | --- | --- |
| PaymentMethod.WebQr | 4 | QR / BNPL, needs back-office activation |
| PaymentMethod.Card | 5 | enabled by default |
| PaymentMethod.InternetBank | 7 | needs activation |
| PaymentMethod.Installment | 8 | needs campaign config; use tbc.installments |
| PaymentMethod.ApplePay | 9 | needs activation |
| PaymentMethod.GooglePay | 14 | needs activation |
Development
npm test # vitest (56 tests, fully mocked — no network)
npm run typecheck # tsc --noEmit
npm run build # tsup → dist/ (ESM + CJS + d.ts)Notes & limitations
- TBC has no public sandbox; testing happens on production with small amounts (docs).
baseUrlis configurable for mock servers. - The split-payment endpoint (
/tpay/paymentswith split) isn't wrapped yet; usetbc.request()directly if you need it.
