Skip to content
Ask the docs

Find answers across the QairoPay docs.

Type a question and we'll synthesize an answer from the docs with citations back to the source pages.

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

Terminal window
pnpm add @qairopay/sdk

Initialize

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

ResourceMethods
passescreate, list, retrieve, revoke
passTemplatescreate, list, retrieve
webhookEndpointscreate, list, retrieve, update, delete, rotateSecret
meretrieve

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 pages
for 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 once

List 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 now

Delete 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:

  • onRequest fires exactly once per logical operation, regardless of retry count.
  • onRetry fires before each retry with the failed error and the upcoming attempt number.
  • onResponse fires exactly once at the end — on success or after retries are exhausted (in which case res.error is 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 Authorization header 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 narrowing
if (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):

RuntimeStatusSmoke test
Node.js 20 LTS✅ Supported floorVitest unit + integration
Node.js 22✅ SupportedVitest unit + integration
Node.js 24✅ SupportedVitest unit + integration
Bun 1.x✅ Supportedbun x vitest run against the unit suite
Deno 2.x✅ Supporteddeno check on examples/deno/quickstart.ts
Cloudflare Workers✅ Supportedwrangler 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-browser package; 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, and ConstructEvent.
  • Ruby — community path: openapi-generator with the ruby target (Faraday-based client).
  • PHP — community path: openapi-generator with the php target (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.