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.
Method reference
| Resource | Methods |
|---|---|
passes | create, list, retrieve, revoke |
passTemplates | create, list, retrieve |
webhookEndpoints | create, list, retrieve, update, delete, rotateSecret |
me | retrieve |
Passes
Create a pass
const pass = await qp.passes.create({ template_id: "tpl_01HXYZ", holder: { email: "ada@example.com", name: "Ada Lovelace" },});console.log(pass.download?.apple_url);List passes (auto-paginate)
// yields every pass across all pagesfor await (const pass of qp.passes.list({ status: "installed" })) { console.log(pass.id, pass.holder.email);}Retrieve a pass
const pass = await qp.passes.retrieve("pss_01HABC");Webhook endpoints
Create an endpoint
const endpoint = await qp.webhookEndpoints.create({ url: "https://hooks.example.com/qairopay", enabled_events: ["pass.installed", "pass.revoked", "payment.authorized"],});console.log(endpoint.signing_secret); // save this — shown onceList endpoints
for await (const ep of qp.webhookEndpoints.list()) { console.log(ep.id, ep.url, ep.status);}Rotate a signing secret
The old secret remains valid for 10 minutes after rotation to allow in-flight requests to complete.
const result = await qp.webhookEndpoints.rotateSecret("whe_01HABC");console.log(result.signing_secret); // new secret — update your env var nowDelete an endpoint
await qp.webhookEndpoints.delete("whe_01HABC");Issuing a pass (pass templates)
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.// Always pass the raw request body bytes, not JSON.stringify(req.body).const event = await QairoPay.webhooks.constructEvent( rawBody, req.headers['QairoPay-Signature'], 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 + full jitter (base 500 ms, max 8 s) up to 3 attempts. Same Idempotency-Key is reused on every retry so writes can never duplicate.
To override the retry budget for an entire QairoPay instance:
const qp = new QairoPay({ apiKey: process.env.QAIROPAY_KEY!, maxRetries: 0, // disable retries entirely});The default request timeout is 30 seconds, applied to the full logical operation (including any retries within budget). Override per-call:
await qp.passes.list({}, { timeoutMs: 60_000 });When the timeout fires, the SDK aborts pending fetches and throws TimeoutError (a subclass of QairoPayError) with the configured timeoutMs attached.
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
- Python — the official Python SDK is available on PyPI (
pip install qairopay). Same resources, same exception hierarchy, sync and async clients. - Go — the official Go SDK is available (
go get github.com/qairopay/go-sdk). Covers passes, payments, webhook subscriptions, andConstructEvent. - Ruby — community path:
openapi-generatorwith therubytarget (Faraday-based client). - PHP — community path:
openapi-generatorwith thephptarget (PSR-7/PSR-18 compatible).
If you’ve built a community client we should know about, email developers@qairopay.com and we’ll list it here.
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.