tokenio-client
v1.0.0
Published
Production-grade TypeScript client for the Token.io Open Banking platform. Covers all 16 APIs: Payments v2, VRP, AIS, Banks, Refunds, Payouts, Settlement, Transfers, Tokens, Token Requests, Account on File, Sub-TPPs, Auth Keys, Reports, Webhooks, and Veri
Maintainers
Readme
tokenio-client
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
Installation
npm install tokenio-client
# or
pnpm add tokenio-client
# or
yarn add tokenio-clientNode.js ≥ 18 required (uses native fetch, crypto.subtle, crypto.getRandomValues).
Quick Start
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);API Coverage
| 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 |
Variable Recurring Payments (VRP)
// 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",
});Webhook Verification
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 });
});Error Handling
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;
}Error properties
| 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 |
Configuration
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
})Telemetry
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.
Testing
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);
});License
MIT — see LICENSE.
