npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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's crypto module.
  • Dual ESM/CJS with full .d.ts type 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-Info fraud-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-sdk

Quick 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-Key is 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 every POST/PUT/PATCH unless 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 test

The 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 build

License

MIT