tipalti-sdk
v1.0.0
Published
A production-grade TypeScript client for the full Tipalti API surface: the modern OAuth2 REST API, the legacy HMAC-signed SOAP Payee/Payer API, and the Procurement REST API.
Maintainers
Readme
tipalti-sdk
A production-grade TypeScript client for the full Tipalti API surface: the modern OAuth2 REST API, the legacy HMAC-signed SOAP Payee/Payer API, and the Procurement REST API.
Zero runtime dependencies. Built entirely on Web-standard APIs — the
native fetch, crypto.subtle (Web Crypto), URLSearchParams,
AbortController — so it works unmodified in Node 18+, browsers, and
edge runtimes (Cloudflare Workers, Deno, Bun) alike. Ships as dual
ESM/CJS with full TypeScript declarations.
Installation
npm install tipalti-sdkQuick start
import { TipaltiClient } from 'tipalti-sdk';
const client = new TipaltiClient({
mode: 'sandbox',
rest: { clientId: '...', clientSecret: '...' },
soap: { payerName: '...', apiKey: '...' },
procurement: { apiKey: '...' },
});
// Modern REST API
const page = await client.payees.list();
for await (const payee of client.payees.stream()) {
console.log(payee.id, payee['name']);
}
// Legacy SOAP API
const result = await client.soap.payer.processPayments(
[{ idap: 'vendor-123', amount: 100.0, currency: 'USD', refCode: 'pay-1' }],
{ paymentGroupTitle: 'Weekly payout' },
);
// Procurement REST API
const pos = await client.procurement.purchaseOrders.list();Only populate the credential sections you actually use — a client built
with just soap is fine as long as you only call client.soap.*
methods. Unconfigured API families are undefined on the client
instance (and typed that way), so accessing them without configuring
them is a compile-time error, not a runtime surprise.
Error handling
Every error extends TipaltiError, so instanceof works uniformly, and
each subclass carries the details specific to its failure kind:
import { AuthenticationError, RateLimitError, ValidationError, SoapFaultError } from 'tipalti-sdk';
try {
await client.payees.get('p_123');
} catch (err) {
if (err instanceof AuthenticationError) {
// bad/expired credentials
} else if (err instanceof RateLimitError) {
// rate limited; err.retryAfterMs has the hint, if any
} else if (err instanceof ValidationError) {
// malformed request; err.errors has field-level details
} else if (err instanceof SoapFaultError) {
// SOAP <soap:Fault>; err.faultCode / err.faultString
} else {
throw err;
}
}Pagination
Every REST list method has a matching stream() returning a native
AsyncGenerator — use for await...of, or the collect helper:
import { collect } from 'tipalti-sdk';
for await (const invoice of client.invoices.stream({ status: 'pending' })) {
// ...
}
const all = await collect(client.invoices.stream({ status: 'pending' }));SOAP signing
HMAC-SHA256 request signing (built on crypto.subtle, not a
Node-specific crypto module) is handled automatically — every
client.soap.payee/client.soap.payer method knows its operation's EAT
(Encryption Additional Terms) parameter and folds it into the signed
request for you. All 45 legacy operations (21 Payee + 24 Payer) are
covered.
Procurement employee import
import { readFile } from 'node:fs/promises';
const csv = await readFile('employees.csv', 'utf8');
await client.procurement.employees.importEmployees(csv);IPN webhooks
import { parseWebhook, webhookEventType } from 'tipalti-sdk';
app.post('/webhooks/tipalti', async (req, res) => {
const event = parseWebhook(await req.text());
handleEvent(webhookEventType(event), event);
res.sendStatus(200);
});Telemetry
const client = new TipaltiClient({
rest: { clientId, clientSecret },
onRequest: (event) => {
console.log(
`${event.api}.${event.operation} -> ${event.status ?? event.error} in ${event.durationMs}ms`,
);
},
});Rate limiting
import { RateLimiter } from 'tipalti-sdk';
const limiter = new RateLimiter(5, 60_000); // Procurement API's documented PO-update limit
await limiter.wait();
await client.procurement.purchaseOrders.update(attrs);Design notes
- HTTP transport: built on native
fetchwithAbortController-based timeouts, exponential backoff + full jitter on retries, and a pluggableonRequesttelemetry hook — no HTTP client dependency. - XML: SOAP responses are parsed with a small, purpose-built
tokenizer (
src/soap/xml.ts) rather than a general XML library — handles the nested-element/self-closing-tag/entity subset Tipalti's SOAP API actually produces, with namespace-prefix stripping so<soap:Fault>matches the same waylocal-name()would in XPath. Not a general-purpose XML parser by design. - REST endpoint shapes (
Payees/Invoices/Payments) follow Tipalti's documented conventions for the modern REST API — see the doc comment onsrc/rest/client.tsif your instance's exact response envelope differs; the request/auth/error-handling machinery there is meant to be reused as-is. - Custom
fetchimplementation supported viaTipaltiConfig.fetch, for non-standard runtimes or test injection.
Quality
npm run typecheck # tsc --noEmit against both src/ and test/ (strict mode)
npm run lint # eslint, typescript-eslint strict+stylistic type-checked rulesets
npm run format:check # prettier
npm run test # vitest — 105 tests
npm run test:coverage # vitest --coverage
npm run build # tsup — dual ESM/CJS + .d.tsAll clean, verified in a fresh checkout. Test coverage is ~84%
statements overall — REST resource modules and the shared HTTP/OAuth2/
config layers are fully covered; the legacy SOAP Payee/Payer wrapper
classes (45 thin methods total) are covered by a representative sample
per class (idap handling, EAT-parameter extraction, nested/repeated
field rendering) rather than one test per method, since they're
low-risk, uniform pass-throughs onto the already-thoroughly-tested
SoapClient.call engine.
Testing this package
The test suite uses a small node:http-based mock server
(test/support/mockServer.ts) instead of nock/msw, keeping the
dependency list minimal even for development/test.
npm testLicense
MIT
