/** * @qbirr/sdk — Node.js client for the qbirr payment verification API. * * import { Qbirr } from '@qbirr/sdk'; * const qb = new Qbirr({ apiKey: process.env.QBIRR_API_KEY! }); * const r = await qb.verify({ * provider: 'cbe', * ref: 'FT23001234ABC', * amount: 500, * receiverName: 'YOUR NAME', * receiverAccount: '1000017692643', * }); * console.log(r.verified, r.payer); * * Works on Node 18+ (uses the built-in fetch). No dependencies. */ /** Thrown for network failures, HTTP errors, or non-JSON responses. */ export class QbirrError extends Error { status; body; constructor(message, status, body) { super(message); this.status = status; this.body = body; this.name = 'QbirrError'; } } const DEFAULT_BASE_URL = 'https://verify.qbirr.com'; export class Qbirr { apiKey; baseUrl; timeoutMs; fetchFn; constructor(opts) { if (!opts.apiKey) throw new QbirrError('apiKey is required'); this.apiKey = opts.apiKey; this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ''); this.timeoutMs = opts.timeoutMs ?? 30_000; this.fetchFn = opts.fetch ?? globalThis.fetch; if (!this.fetchFn) throw new QbirrError('fetch is not available — pass options.fetch on Node < 18'); } /** * Verify a single transaction. Returns `{ verified: false, error: '...' }` * on a real verification failure (wrong amount, wrong receiver, expired * receipt). Throws QbirrError for network/auth problems. */ async verify(req) { const body = { provider: req.provider, ref: req.ref, amount: req.amount, receiver_name: req.receiverName, }; if (req.receiverAccount) body.receiver_account = req.receiverAccount; const data = await this.request('POST', '/api/v1/verify', body); return data; } /** * Verify by uploading the receipt itself — PDF or an image (PNG/JPEG). * Images with a QR code (most bank receipts have one) are decoded and * routed through the URL-based flow automatically. * * const buf = await fs.readFile('receipt.pdf'); * await qb.verifyUpload({ * provider: 'cbe', * amount: 999, * receiverName: 'YOUR NAME', * receiverAccount: '1000017692643', * file: buf, * filename: 'receipt.pdf', * contentType: 'application/pdf', * }); */ async verifyUpload(opts) { const form = new FormData(); form.append('provider', opts.provider); form.append('amount', String(opts.amount)); form.append('receiver_name', opts.receiverName); if (opts.receiverAccount) form.append('receiver_account', opts.receiverAccount); const blob = opts.file instanceof Blob ? opts.file : new Blob([opts.file], { type: opts.contentType || 'application/octet-stream' }); form.append('file', blob, opts.filename || 'upload.pdf'); return this.requestForm('POST', '/api/v1/verify/upload', form); } /** Returns the current month's quota usage for this API key. */ async usage() { return this.request('GET', '/api/v1/usage'); } async requestForm(method, path, form) { const url = `${this.baseUrl}${path}`; const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), this.timeoutMs); try { const resp = await this.fetchFn(url, { method, headers: { 'X-API-Key': this.apiKey, 'User-Agent': '@qbirr/sdk-node/1.0.0' }, body: form, signal: ctrl.signal, }); const text = await resp.text(); let parsed; try { parsed = text ? JSON.parse(text) : {}; } catch { throw new QbirrError(`Non-JSON response from qbirr (HTTP ${resp.status})`, resp.status, text); } if (!resp.ok) throw new QbirrError(parsed?.error || `HTTP ${resp.status}`, resp.status, parsed); return parsed; } catch (e) { if (e instanceof QbirrError) throw e; if (e?.name === 'AbortError') throw new QbirrError(`Upload timed out after ${this.timeoutMs}ms`); throw new QbirrError(`Network error: ${e.message}`); } finally { clearTimeout(timer); } } async request(method, path, body) { const url = `${this.baseUrl}${path}`; const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), this.timeoutMs); try { const resp = await this.fetchFn(url, { method, headers: { 'X-API-Key': this.apiKey, 'Content-Type': 'application/json', Accept: 'application/json', 'User-Agent': '@qbirr/sdk-node/1.0.0', }, body: body ? JSON.stringify(body) : undefined, signal: ctrl.signal, }); let parsed; const text = await resp.text(); try { parsed = text ? JSON.parse(text) : {}; } catch { throw new QbirrError(`Non-JSON response from qbirr (HTTP ${resp.status})`, resp.status, text); } if (!resp.ok) { const msg = parsed?.error || `HTTP ${resp.status}`; throw new QbirrError(msg, resp.status, parsed); } return parsed; } catch (e) { if (e instanceof QbirrError) throw e; if (e?.name === 'AbortError') throw new QbirrError(`Request timed out after ${this.timeoutMs}ms`); throw new QbirrError(`Network error: ${e.message}`); } finally { clearTimeout(timer); } } } export default Qbirr;