synctera-sdk
v1.0.0
Published
Unofficial, production-grade TypeScript SDK for the Synctera Banking-as-a-Service API — typed resource clients, tiered idempotency, cursor pagination, webhook signature verification, and automatic retries.
Maintainers
Readme
synctera-sdk
An unofficial, production-grade TypeScript SDK for the Synctera Banking-as-a-Service API.
- Fully typed — 457 generated types and 174 endpoint methods across 28 resource groups, generated directly from Synctera's OpenAPI spec (never hand-guessed).
- Zero runtime dependencies — built on native
fetch,AbortController, and Node'scryptomodule. - Dual ESM/CJS with full
.d.tstype declarations. - Correct idempotency semantics — Synctera has two genuinely different idempotency behaviors depending on the endpoint, and this SDK models both instead of pretending they're the same (see Idempotency below).
- Automatic retries with exponential backoff + jitter, honoring
Retry-After, limited strictly to the status codes Synctera's docs confirm are safe to retry (429 and 5xx). - Webhook signature verification — HMAC-SHA256, secret-rotation support, replay-window protection.
- Cursor pagination helpers as async iterators.
Customer-Device-Infofraud-fingerprinting header built for you from a typed object.
⚠️ Important: spec currency
This SDK is generated from the most recent machine-readable OpenAPI spec I could obtain — the snapshot Synctera checks into their own client-libraries-go scaffold repo. That snapshot covers 114 paths / 174 operations across every core domain (customers, persons, businesses, accounts, cards, ACH, wires, internal transfers, transactions, documents, webhooks, KYC/KYB verification, watchlist monitoring, external accounts/cards, payment schedules, remote check deposit, reconciliations, applications, and sandbox simulations).
It does not yet include a handful of newer resources visible in Synctera's live docs but not present in that spec snapshot: Spend Controls, Evaluation Overrides, Merchants, Institutions, Enhanced Due Diligence (EDD), Customer Risk Rating (CRR), and Incoming Wires as a standalone resource. If you need these, see Regenerating against an updated spec — the codegen pipeline is designed so adding them is a one-command operation once you have an updated openapi.json, not a rewrite.
Install
npm install synctera-sdkQuick start
import { SyncteraClient } from "synctera-sdk";
const client = new SyncteraClient({
apiKey: process.env.SYNCTERA_API_KEY, // or omit and set SYNCTERA_API_KEY in the environment
environment: "sandbox", // "sandbox" | "tMinus10" | "production" — default "sandbox"
});
const person = await client.persons.createPerson({
body: {
first_name: "Ada",
last_name: "Lovelace",
email: "[email protected]",
},
});Idempotency
Synctera has two distinct idempotency behaviors, and this SDK does not paper over the difference:
Standard endpoints (most of the API): an
Idempotency-Keyis optional. A repeated request with the same key returns the original cached response for 7 days. This SDK auto-generates a key for you on everyPOST/PUT/PATCHunless you supply one — it's always safe to retry.Ledger / money-movement endpoints (ACH, wires, internal transfers, RDC deposits): the key is required by Synctera, and a repeat with the same key is never served from cache — it's rejected with a 409 or 422. Because a silently-retried request would fail loudly here instead of replaying safely, this SDK will not auto-generate a key for these calls by default:
// Throws SyncteraUsageError — you must supply a key explicitly for ledger calls:
await client.ach.addTransactionOut({ body: { ... } });
// Correct: mint one key per logical operation, reuse it only when retrying that same operation.
const idempotencyKey = crypto.randomUUID();
await client.ach.addTransactionOut({ body: { ... }, idempotencyKey });If you understand the tradeoff and want auto-generation on ledger calls anyway (e.g. you're doing your own outer-level retry coordination), opt in:
const client = new SyncteraClient({ autoGenerateLedgerIdempotencyKeys: true });Pagination
All list endpoints return next_page_token. Use paginate for a lazy async iterator, or collectAll to gather everything into an array:
import { paginate, collectAll } from "synctera-sdk";
for await (const person of paginate(
(params) => client.persons.listPersons({ query: params }),
(page) => page.persons ?? [],
)) {
console.log(person.id);
}
const allBusinesses = await collectAll(
(params) => client.businesses.listBusinesses({ query: params }),
(page) => page.businesses ?? [],
);Webhook verification
import { verifyWebhookSignature, SyncteraWebhookSignatureError } from "synctera-sdk";
app.post("/webhooks/synctera", express.raw({ type: "application/json" }), (req, res) => {
try {
verifyWebhookSignature({
payload: req.body, // raw Buffer/string — do NOT JSON.parse before verifying
signatureHeader: req.header("synctera-signature") ?? "",
timestampHeader: req.header("synctera-timestamp") ?? "",
secret: process.env.SYNCTERA_WEBHOOK_SECRET!,
// previousSecret: process.env.SYNCTERA_WEBHOOK_SECRET_PREVIOUS, // during rotation
});
} catch (err) {
if (err instanceof SyncteraWebhookSignatureError) return res.status(400).send("Invalid signature");
throw err;
}
const event = JSON.parse(req.body);
// ... handle event
res.sendStatus(200);
});Device fingerprinting
await client.accounts.getAccount({
path: { account_id: id },
deviceInfo: { customerId: person.id!, ipAddress: req.ip, userAgent: req.get("user-agent") },
});Or set it once for a whole server-side context (e.g. a batch job) via defaultDeviceInfo in the client config.
Error handling
import {
SyncteraNotFoundError,
SyncteraValidationError,
SyncteraConflictError,
SyncteraRateLimitError,
} from "synctera-sdk";
try {
await client.persons.getPerson({ path: { person_id: id } });
} catch (err) {
if (err instanceof SyncteraNotFoundError) {
// 404
} else if (err instanceof SyncteraValidationError) {
// 422 — includes IDEMPOTENCY_INVALID_REUSE (same key, different payload)
console.log(err.code, err.body);
} else if (err instanceof SyncteraConflictError) {
// 409 — includes IDEMPOTENCY_CONCURRENT_USE
} else if (err instanceof SyncteraRateLimitError) {
// 429 — already retried internally up to your configured limit
}
throw err;
}Every error carries .status, .code (Synctera's machine-readable error code, when present), .body, .requestId, .url, and .method.
Configuration
new SyncteraClient({
apiKey: "...",
environment: "sandbox", // or "tMinus10" | "production"
baseUrl: undefined, // override entirely, e.g. for a mock server in tests
timeoutMs: 30_000,
retry: { maxRetries: 2, baseDelayMs: 250, maxDelayMs: 8000 },
autoGenerateLedgerIdempotencyKeys: false,
defaultDeviceInfo: { customerId: "...", ipAddress: "..." },
defaultHeaders: {},
onResponse: ({ method, url, status, requestId, durationMs }) => {
// hook for logging/metrics
},
});Sandbox testing helpers
The sandbox environment exposes simulation endpoints for card authorizations, clearing, reversals, ACH/wire returns, and more — all covered under client.cardTransactionSimulations, client.cardWebhookSimulations, etc. client.sandboxWipe.wipeWorkspace() resets your sandbox workspace to a clean slate (throws outside sandbox).
Regenerating against an updated spec
The entire typed surface (src/generated/) is produced by scripts/generate.ts from an OpenAPI document. To pick up new Synctera resources or fields:
# Drop an updated spec at reference/openapi.json, then:
npm run generate # regenerates src/generated/types.ts and src/generated/resources/*.ts
npm run typecheck
npm testThe generator resolves $ref parameters, allOf/oneOf/anyOf compositions, and array/object schemas. It intentionally falls back to unknown rather than guessing when it encounters a pattern it doesn't recognize, so a spec update never silently produces an incorrect type — a fallback to unknown is your signal to open scripts/generate.ts and extend the converter.
Development
npm install
npm run generate # spec -> types + resources
npm run lint
npm run typecheck
npm test
npm run buildLicense
MIT
