triple-sdk
v1.0.0
Published
TypeScript client for the Triple (jointriple.com) transaction data enrichment API
Maintainers
Readme
triple-sdk
A TypeScript client for the Triple transaction data enrichment API: turn raw bank/card transaction strings into clean merchant names, logos, categories, locations, contact details, subscription detection, CO₂ estimates, fraud signals, and payment processor identification.
Zero runtime dependencies. Isomorphic — works in Node.js 18.17+, browsers,
and edge runtimes (Cloudflare Workers, Vercel Edge, Deno, Bun), built
entirely on native fetch, Headers, AbortController, and Web Crypto.
Ships as dual ESM/CJS with bundled type declarations.
Installation
npm install triple-sdkQuick start
import { TripleClient } from 'triple-sdk';
const client = new TripleClient({ apiKey: process.env.TRIPLE_API_KEY! });
const enriched = await client.enrich.transaction({
merchantName: 'AMZN MKTP UK',
transactionType: 'CARD_TRANSACTION',
transactionId: crypto.randomUUID(),
merchantCountry: 'GBR',
transactionAmount: 24.99,
transactionCurrency: 'GBP',
channelType: 'ECOMMERCE',
});
console.log(enriched.visual_enrichments?.merchant_clean_name); // "Amazon"A TripleClient is safe to use concurrently, and safe to build more than
one of (e.g. one per tenant, or one for sandbox alongside one for
production) side by side in the same process — there's no shared module
state.
API keys starting with tr_test_ are sandbox keys, tr_live_ are
production keys — the environment (and therefore which host gets called)
is inferred automatically from whichever you pass in, unless you set
environment explicitly.
Configuration
Every option is a plain field on the object passed to new TripleClient(...):
const client = new TripleClient({
apiKey: process.env.TRIPLE_API_KEY!,
receiveTimeoutMs: 15_000,
maxRetries: 5,
});Or set the environment explicitly instead of relying on key-prefix inference:
const client = new TripleClient({ apiKey, environment: 'sandbox' });See the TripleConfigInput type for the full list — timeouts, retry
policy, a custom fetch passthrough (handy for testing or non-global-fetch
runtimes), an optional client-side rate limiter, telemetry hooks, and so on.
Enrichment
Two flavours, matching Triple's two enrichment endpoints:
// Structured — when you have discrete fields
await client.enrich.transaction({
merchantName: 'AMZN MKTP UK',
transactionType: 'CARD_TRANSACTION',
transactionId: generateTransactionId(),
merchantCountry: 'GBR',
transactionAmount: 24.99,
transactionCurrency: 'GBP',
});
// Unstructured — when all you have is a raw description string
await client.enrich.unstructuredTransaction({
transactionId: generateTransactionId(),
text: 'CRD PUR 4321 NETFLIX.COM 866-5797172 CA',
transactionAmount: 15.99,
transactionCurrency: 'USD',
});Every input is validated locally before any network call is made — invalid
input rejects with a TripleError whose type is 'validation'
immediately, with the same field-level error shape (errors: Record<string,
string[]>) Triple's own API would return.
The two response shapes differ slightly, matching Triple's own OpenAPI
spec: the structured response wraps every enrichment feature (location,
subscriptions, CO₂, fraud, contact, payment processor) in an
enabled-flagged object, since not every transaction carries every kind of
signal — an online purchase, for instance, never has a merchant_location.
The unstructured response uses flat, simply-nullable objects instead. Two
small helpers smooth over the structured shape: isRecurring(subscriptions)
and isFlagged(fraud).
Brands, feedback, stocks, cryptos, and TLS
// Look up a brand directly (e.g. to refresh a cached logo)
await client.brands.fetch('497f6eca-6276-4993-bfeb-53cbbbba6f08');
// Tell Triple when enrichment data is wrong or missing
await client.feedback.report({
transactionId: 'txn_123',
report: 'brand_name',
responseValue: 'AMZN MKTP UK',
feedback: 'Should be Amazon',
});
// Brokerage data
await client.stocks.fetch('LU1778762911', { format: 'svg_light' });
await client.cryptos.fetch('bitcoin');
// Issue an mTLS client certificate (hits Triple's control-plane host)
await client.tls.issueCertificate({ publicKey: pem, lifetime: 365 });Error handling
Every call either resolves with its result or rejects with a TripleError
— there is no other rejection shape to guard against:
import { TripleError } from 'triple-sdk';
try {
const enriched = await client.enrich.transaction(req);
} catch (err) {
if (err instanceof TripleError) {
switch (err.type) {
case 'validation':
// local validation failure — err.errors is a field -> messages[] map
console.error('bad enrich payload:', err.errors);
break;
case 'rate_limited':
// only seen after the client's own retries are exhausted
console.error('rate limited, retry after', err.retryAfter);
break;
default:
console.error(err);
}
}
}TripleError.type distinguishes 'validation', 'unauthenticated',
'forbidden', 'not_found', 'rate_limited', 'server_error',
'unexpected_status', and 'network_error' — see the exported
TripleErrorType for the full list, and TripleError.status,
.errors, .retryAfter, .rawBody, and .cause for the rest.
Retries
408, 429, 500, 502, 503, and 504 responses (and transport
errors) are retried automatically with exponential backoff, honoring
Triple's Retry-After header on 429s. Configure or disable this via
maxRetries / shouldRetry:
new TripleClient({ apiKey, maxRetries: 5 });
// Disable retries entirely:
new TripleClient({ apiKey, shouldRetry: () => ({ retry: false, delayMs: 0 }) });Telemetry
A TelemetryHook receives a TelemetryEvent around every request attempt
(start/stop/error) — handy for logging, metrics, or tracing:
new TripleClient({
apiKey,
hooks: [
(event) => {
console.log(`${event.method} ${event.path} attempt=${event.attempt} status=${event.status}`);
},
],
});Optional client-side rate limiting
For bulk workloads (e.g. backfilling historical transactions) where you'd
rather avoid 429s in the first place:
import { TokenBucketRateLimiter } from 'triple-sdk';
const rateLimiter = new TokenBucketRateLimiter(50, 1000); // 50 requests/second
const client = new TripleClient({ apiKey, rateLimiter });This is a single-process token bucket. For multi-instance deployments
sharing one rate budget, implement the RateLimiter interface yourself
(e.g. backed by Redis).
Testing code that calls Triple
TripleConfigInput.fetch lets you swap in any fetch-compatible function, so
tests never need to hit the network:
const client = new TripleClient({
apiKey: 'tr_test_xxx',
fetch: async () =>
new Response(JSON.stringify({ transaction_id: 'txn_1' }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
});Cancellation
Every method accepts an optional trailing AbortSignal:
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
await client.enrich.transaction(req, controller.signal);Sandbox vs. production
Triple provides fully isolated sandbox and production environments (API
hosts, dashboards, and databases). Pass a tr_test_* key to hit sandbox,
or tr_live_* for production — the SDK infers this and warns on any
mismatch if you also pass environment explicitly.
Development
npm install
npm run typecheck # tsc --noEmit
npm run lint # eslint .
npm test # vitest run
npm run test:coverage
npm run build # vite build -> dist/License
MIT. See LICENSE.
Disclaimer
This is a community-maintained client and is not officially affiliated with or endorsed by Triple Technologies. See jointriple.com for the official product and docs.triple.app for the official API reference.
