Production-grade TypeScript client for the Token.io Open Banking platform.
16 APIs — full TypeScript types — automatic OAuth2 token caching — retry with jitter — HMAC webhook verification — ESM + CJS
npm install tokenio-client
# or
pnpm add tokenio-client
# or
yarn add tokenio-clientNode.js ≥ 18 required (uses native fetch, crypto.subtle, crypto.getRandomValues).
import { TokenioClient } from "tokenio-client";
const client = new TokenioClient({
clientId: process.env.TOKENIO_CLIENT_ID!,
clientSecret: process.env.TOKENIO_CLIENT_SECRET!,
// environment: "production", // defaults to "sandbox"
});
// Initiate a payment
const payment = await client.payments.initiate({
initiation: {
bankId: "ob-modelo",
amount: { value: "10.50", currency: "GBP" },
creditor: { accountNumber: "12345678", sortCode: "040004", name: "Acme Ltd" },
remittanceInformationPrimary: "Invoice #42",
callbackUrl: "https://yourapp.com/payment/return",
returnRefundAccount: true,
},
});
// Handle the auth flow
if (payment.requiresRedirect()) {
console.log("Redirect PSU to:", payment.redirectUrl);
} else if (payment.requiresEmbeddedAuth()) {
console.log("Collect fields:", payment.embeddedAuthFields);
}
// Poll to final (prefer webhooks in production)
const final = await client.payments.pollUntilFinal(payment.id, {
intervalMs: 2_000,
timeoutMs: 60_000,
});
console.log("Payment status:", final.status);| Namespace | Methods |
|---|---|
client.payments |
initiate, get, list, getWithTimeout, provideEmbeddedAuth, generateQRCode, pollUntilFinal |
client.vrp |
createConsent, getConsent, listConsents, revokeConsent, listConsentPayments, createPayment, getPayment, listPayments, confirmFunds |
client.ais |
listAccounts, getAccount, listBalances, getBalance, listTransactions, getTransaction, listStandingOrders, getStandingOrder |
client.banks |
listV1, listV2, listCountries |
client.refunds |
initiate, get, list |
client.payouts |
initiate, get, list |
client.settlement |
createAccount, listAccounts, getAccount, listTransactions, getTransaction, createRule, listRules, deleteRule |
client.transfers |
redeem, get, list |
client.tokens |
list, get, cancel |
client.tokenRequests |
store, get, getResult, initiateBankAuth |
client.accountOnFile |
create, get, delete |
client.subTpps |
create, list, get, delete |
client.authKeys |
submit, list, get, delete |
client.reports |
listBankStatuses, getBankStatus |
client.webhooks |
setConfig, getConfig, deleteConfig, parse, parseOrThrow, typed decoders |
client.verification |
initiate |
// 1. Create consent
const consent = await client.vrp.createConsent({
bankId: "ob-modelo",
currency: "GBP",
creditor: { accountNumber: "12345678", sortCode: "040004", name: "Acme" },
maximumIndividualAmount: "500.00",
periodicLimits: [
{ maximumAmount: "1000.00", periodType: "MONTH", periodAlignment: "CALENDAR" },
],
callbackUrl: "https://yourapp.com/vrp/return",
});
// 2. Redirect PSU to authorize
if (consent.requiresRedirect()) {
redirect(consent.redirectUrl!);
}
// 3. Confirm funds (optional)
const hasFunds = await client.vrp.confirmFunds(consent.id, "49.99");
// 4. Initiate payment against authorized consent
const payment = await client.vrp.createPayment({
consentId: consent.id,
amount: { value: "49.99", currency: "GBP" },
remittanceInformationPrimary: "Monthly subscription",
});const client = new TokenioClient({
staticToken: "...",
webhookSecret: process.env.TOKENIO_WEBHOOK_SECRET,
});
// Express handler
app.post("/webhooks/tokenio", express.raw({ type: "application/json" }), async (req, res) => {
const sig = req.headers["x-token-signature"] as string;
const result = await client.webhooks.parse(req.body, sig);
if (!result.ok) {
res.status(401).json({ error: result.error });
return;
}
const { event } = result;
switch (event.type) {
case "payment.completed": {
const data = client.webhooks.decodePaymentData(event);
await handlePaymentCompleted(data.paymentId, data.status);
break;
}
case "vrp.completed": {
const data = client.webhooks.decodeVRPData(event);
await handleVRPCompleted(data.vrpId, data.consentId);
break;
}
}
res.status(200).json({ received: true });
});All methods throw TokenioError on failure:
import { TokenioError } from "tokenio-client";
try {
const payment = await client.payments.get(paymentId);
} catch (err) {
if (err instanceof TokenioError) {
switch (err.code) {
case "not_found":
return null;
case "rate_limit_exceeded":
await sleep((err.retryAfter ?? 5) * 1000);
return retry();
case "unauthorized":
throw new Error("Check your API credentials");
default:
logger.error("Token.io error", { code: err.code, status: err.status, traceId: err.requestId });
throw err;
}
}
throw err;
}| Property | Type | Description |
|---|---|---|
code |
ErrorCode |
Machine-readable code |
message |
string |
Human-readable description |
status |
number |
HTTP status (0 for client-side) |
requestId |
string? |
X-Request-ID trace header |
retryAfter |
number? |
Retry-After header in seconds |
isRetryable |
boolean |
True for 429/5xx |
isNotFound |
boolean |
True for 404 |
isUnauthorized |
boolean |
True for 401 |
isRateLimited |
boolean |
True for 429 |
new TokenioClient({
clientId: "...", // OAuth2 client ID
clientSecret: "...", // OAuth2 client secret
staticToken: "...", // Bypass OAuth2 (tests only)
environment: "sandbox", // "sandbox" | "production"
baseUrl: "...", // Override API URL
timeoutMs: 30_000, // Per-request timeout
maxRetries: 3, // Max retries on 5xx/429
retryWaitMinMs: 500, // Min retry backoff
retryWaitMaxMs: 5_000, // Max retry backoff
webhookSecret: "...", // HMAC webhook secret
logger: myLogger, // Custom logger
})Attach a global handler to receive telemetry events:
(globalThis as any).__tokenioTelemetry = (event) => {
metrics.histogram("tokenio.request.duration", event.duration, {
method: event.method,
path: event.path,
status: String(event.status),
});
};Event fields: event, method, path, status, duration (ms), timestamp.
import { vi } from "vitest";
import { TokenioClient, clearTokenCache } from "tokenio-client";
beforeEach(() => {
clearTokenCache();
vi.stubGlobal("fetch", vi.fn(async () =>
new Response(JSON.stringify({
payment: { id: "pm:test", status: "INITIATION_COMPLETED" }
}), { status: 200, headers: { "content-type": "application/json" } })
));
});
it("gets a payment", async () => {
const client = new TokenioClient({ staticToken: "test-token" });
const payment = await client.payments.get("pm:test");
expect(payment.id).toBe("pm:test");
expect(payment.isFinal()).toBe(true);
});MIT — see LICENSE.