dhanavega-sdk
v0.1.0
Published
Official TypeScript client for the Dhanavega API — generated types (openapi-typescript) over a thin openapi-fetch runtime core (auth, Idempotency-Key, problem+json, pagination). See issue #41, ADR 0037.
Readme
dhanavega-sdk
Official TypeScript client for the Dhanavega API. Generated types
(openapi-typescript) over a thin openapi-fetch
runtime core — bearer auth, Idempotency-Key autogeneration, RFC 9457 problem+json error parsing,
If-Match concurrency, and cursor pagination. See issue #41 and
ADR 0037 for the design
rationale.
Status: buildable and test-covered; not yet published to npm (issue #41 — publication is a manual step performed by the maintainers once the platform reaches its India-GA milestone). Everything below documents the eventual
npm install dhanavega-sdkexperience.
Install
pnpm add dhanavega-sdkQuickstart
import { createDhanavegaClient } from 'dhanavega-sdk';
const client = createDhanavegaClient({
baseUrl: 'https://api.dhanavega.example',
auth: { token: '…' }, // or auth: { getToken: () => fetchShortLivedToken() }
});
const { data } = await client.los.GET('/los/leads');Each module surface is its own typed openapi-fetch client: client.admin, client.brn, client.chn,
client.cus, client.lcs, client.lms, client.los, client.xc, client.partner. All share the same
middleware stack. Types for a module are also importable standalone via subpath exports, e.g.:
import type { components } from 'dhanavega-sdk/los';
type Application = components['schemas']['Application'];Webhook payload shapes (types only — no runtime client) are available from dhanavega-sdk/webhooks/inbound-provider
and dhanavega-sdk/webhooks/outbound-partner.
Auth
tenant_id is never sent by the client — the API derives it server-side from the bearer JWT
(api-docs/02-auth-tenancy-scopes.md). Supply either a static token or a getToken callback (invoked
fresh on every request, so you can refresh a short-lived token):
createDhanavegaClient({ baseUrl, auth: { getToken: async () => await refreshToken() } });Idempotency
Every POST/PUT/PATCH/DELETE operation's contract requires an Idempotency-Key header
parameter (api-docs/04-idempotency-concurrency-webhooks.md §1) — openapi-typescript therefore types it
as a required params.header field on every mutating call. Use withIdempotencyKey() to satisfy that
type and generate the value in one step:
import { withIdempotencyKey } from 'dhanavega-sdk';
await client.los.POST('/los/applications', {
params: { header: withIdempotencyKey() },
body: { borrower_id, product_id, product_version_id, channel: 'SELF_SERVE', currency_code: 'INR' },
});Pass your own key to deliberately replay an attempt: withIdempotencyKey('my-own-key'). A stored replay
comes back with the Idempotency-Replayed: true header, surfaced on a thrown DhanavegaProblem as
.idempotencyReplayed. (A runtime safety-net middleware also fills the header in automatically at the
Request level if it's ever missing — belt and suspenders, not the primary calling convention.)
Error handling
Every non-2xx response throws a DhanavegaProblem — never a bare non-2xx Response — parsed from the
platform's RFC 9457 application/problem+json envelope (api-docs/03-errors-pagination.md §1). Branch
on .code (a closed DhanavegaErrorCode union), never on .message/detail text:
import { DhanavegaProblem } from 'dhanavega-sdk';
try {
await client.los.POST('/los/applications', { params: { header: withIdempotencyKey() }, body });
} catch (err) {
if (err instanceof DhanavegaProblem) {
if (err.code === 'VALIDATION_FAILED') {
for (const fieldError of err.errors ?? []) console.log(fieldError.pointer, fieldError.rule);
} else if (err.code === 'RATE_LIMITED') {
console.log('retry after', err.retryAfter);
}
}
}Concurrency (If-Match)
import { withIfMatch } from 'dhanavega-sdk';
await client.los.PATCH('/los/applications/{id}', {
params: { path: { id: applicationId } },
body: patch,
...withIfMatch(etag),
});Missing If-Match on a mutable-entity mutation → 428 PRECONDITION_REQUIRED; a stale one → 412
VERSION_CONFLICT.
Pagination
import { paginate } from 'dhanavega-sdk';
for await (const lead of paginate((cursor) =>
client.los
.GET('/los/leads', { params: { query: { 'page[after]': cursor } } })
.then(({ data }) => data!),
)) {
console.log(lead);
}paginate walks page.has_more / page.next_cursor until the stream ends — cursors are opaque; never
parse or construct them yourself.
Regenerating types
pnpm --filter dhanavega-sdk generateRegenerates every committed file under src/generated/ from planning/api-docs/openapi/**. Run this
whenever a contract YAML changes — test/drift.spec.ts fails CI if the committed types fall out of sync.
Publish procedure (manual, first release)
Publication is intentionally not automated yet (issue #41). CI's sdk-release.yml carries a
publish-sdk-npm job gated behind the PUBLISH_ENABLED repo variable, which is unset today. To publish
manually once the maintainers decide to:
pnpm --filter dhanavega-sdk build
pnpm --filter dhanavega-sdk publish --access public --no-git-checks(or from the repo root: pnpm sdk:publish:npm).
Development
pnpm --filter dhanavega-sdk generate # regenerate src/generated/*.ts
pnpm --filter dhanavega-sdk typecheck
pnpm --filter dhanavega-sdk test
pnpm --filter dhanavega-sdk build