TypeScript SDK
The TypeScript SDK is the canonical client for the QairoPay API. It’s fully typed, handles idempotency keys, retries transient errors, verifies webhook signatures, and works across Node, Bun, Deno, and edge runtimes.
Install
pnpm add @qairopay/sdknpm install @qairopay/sdkyarn add @qairopay/sdkbun add @qairopay/sdkInitialize
import { QairoPay } from "@qairopay/sdk";
const qp = new QairoPay({ apiKey: process.env.QAIROPAY_KEY!, // optional — auto-detected from key prefix, override only if needed // baseUrl: "https://api.sandbox.qairopay.com", // apiVersion: "2026-05-19", // maxRetries: 3, // timeoutMs: 15000,});The SDK reads the environment (sandbox vs live) from the key prefix. You don’t need to set baseUrl unless you’re hitting a custom mirror.
Issuing a pass
const template = await qp.passTemplates.create({ name: "Hello Pass", kind: "loyalty", brand: { background_color: "#1F7A5A", foreground_color: "#FCFAF6" },});
// SDK auto-attaches an Idempotency-Key (UUID v4) — pass one explicitly// only if you need to derive it from your own request id.const pass = await qp.passes.create({ template_id: template.id, holder: { email: "you@example.com", name: "Jane Doe" },});
console.log(pass.download.apple_url);Observability hooks
The SDK fires three lifecycle hooks per logical operation (FR-042). They’re the seam for OpenTelemetry, audit logging, and metrics — they fire at the operation boundary, not per raw fetch, so retries don’t double-count.
import { QairoPay } from "@qairopay/sdk";import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("qairopay-sdk");const spans = new Map<string, ReturnType<typeof tracer.startSpan>>();
const qp = new QairoPay({ apiKey: process.env.QAIROPAY_KEY!, hooks: { onRequest: (ctx) => { const span = tracer.startSpan(`qairopay ${ctx.method} ${ctx.url}`, { attributes: { "qairopay.operation_id": ctx.operationId, "qairopay.api_version": ctx.apiVersion, "qairopay.idempotency_key": ctx.idempotencyKey ?? "", }, }); spans.set(ctx.operationId, span); }, onRetry: (ctx, lastError, attempt) => { spans.get(ctx.operationId)?.addEvent("retry", { attempt, error_type: lastError.type, status: lastError.status, }); }, onResponse: (ctx, res) => { const span = spans.get(ctx.operationId); if (!span) return; span.setAttributes({ "http.status_code": res.status, "qairopay.request_id": res.requestId ?? "", "qairopay.attempts": res.attempts, "duration_ms": res.durationMs, }); if (res.error) span.recordException(res.error); span.end(); spans.delete(ctx.operationId); }, },});Guarantees:
onRequestfires exactly once per logical operation, regardless of retry count.onRetryfires before each retry with the failed error and the upcoming attempt number.onResponsefires exactly once at the end — on success or after retries are exhausted (in which caseres.erroris populated).- A hook that throws or returns a rejected Promise is caught — it never breaks the call site.
- Sensitive values are redacted before the context reaches your hook: the
Authorizationheader is masked, card display tokens and provisioning payloads are replaced with placeholders, and webhook signing secrets are masked to***in responses.
Iterating list endpoints
// Auto-paginate — yields every record across every page, lazily.// Break out of the loop and the SDK stops fetching.for await (const pass of qp.passes.list({ status: "installed" })) { console.log(pass.id);}
// Page-aware control — useful for checkpointing or backpressure.const iter = qp.passes.list({ status: "installed" });const page = await iter.page(); // { data, has_more, next_cursor }The iterator handles cursor pagination automatically. Pass limit to control page size (default 25, max 100). See Pagination for the full reference.
Error handling
Errors are typed exceptions. Catch the specific type you care about:
import { QairoPayError, RateLimitError, InvalidParameterError } from "@qairopay/sdk";
try { await qp.passes.create(payload);} catch (err) { if (err instanceof InvalidParameterError) { console.error(`Bad field: ${err.param} — ${err.message}`); } else if (err instanceof RateLimitError) { await sleep(err.retryAfterMs); } else if (err instanceof QairoPayError) { console.error(err.requestId, err.type, err.code); } else { throw err; }}Webhook verification
import { QairoPay } from "@qairopay/sdk";
// constructEvent is async — Web Crypto's HMAC primitives return Promises.const event = await QairoPay.webhooks.constructEvent( rawBody, signatureHeader, process.env.QAIROPAY_WEBHOOK_SECRET!,);
// event is fully typed — switch on event.type for narrowingif (event.type === "pass.installed") { await onPassInstalled(event.data.pass);}Retries and timeouts
By default, the SDK retries idempotent operations on 5xx, 429, and connection errors with exponential backoff up to 3 attempts. To disable retries for a specific call:
await qp.passes.create(payload, { retries: 0 });The default request timeout is 15 seconds. Override per-call:
await qp.passes.list({}, { timeoutMs: 30000 });Edge runtimes
The SDK uses only Web-standard primitives — globalThis.fetch, crypto.subtle, and crypto.randomUUID() — with no node:* imports in the request path. Six runtimes are verified on every CI run (per SC-006):
| Runtime | Status | Smoke test |
|---|---|---|
| Node.js 20 LTS | ✅ Supported floor | Vitest unit + integration |
| Node.js 22 | ✅ Supported | Vitest unit + integration |
| Node.js 24 | ✅ Supported | Vitest unit + integration |
| Bun 1.x | ✅ Supported | bun x vitest run against the unit suite |
| Deno 2.x | ✅ Supported | deno check on examples/deno/quickstart.ts |
| Cloudflare Workers | ✅ Supported | wrangler dev boots examples/cloudflare-worker/worker.ts |
Best-effort (not in CI matrix)
- Vercel Edge Runtime — shares the V8 Isolate model with Cloudflare Workers; the SDK code paths are byte-identical. Works out of the box, but isn’t on the formal smoke matrix in v1.
Out of scope for v1.0
- Browsers. A secret API key in a browser bundle is unsafe — the key would be visible to any visitor. A publishable-key-only browser surface is on the roadmap as a separate
@qairopay/sdk-browserpackage; the current package is server-side only.
Other languages
Official SDKs are in active development for Python, Go, Ruby, and PHP. Until those ship, the OpenAPI spec is a working source for any code generator (we recommend openapi-typescript or oapi-codegen).
Source and issues
The SDK is open source on GitHub at qairopay/qairopay-node. Bug reports and pull requests welcome. Security issues should go to security@qairopay.com, not the public tracker.