# @vouch.finance/sdk

TypeScript SDK for the Vouch Purpose-Bound Voucher platform. Supports UPI QR voucher redemption, payment intents, voucher balance checks, and verification.

Two entry points:

- `@vouch.finance/sdk` — core client, UPI parsing, framework-agnostic scanner
- `@vouch.finance/sdk/react` — React QR scanner component

## Install

```bash
npm install @vouch.finance/sdk
```

If using the QR scanner, also install the optional peer dependencies:

```bash
# React apps using <QRScanner />
npm install react react-dom html5-qrcode

# Non-React apps using UPIScanner
npm install html5-qrcode
```

CDN (no bundler):

```html
<script src="https://cdn.jsdelivr.net/npm/@vouch.finance/sdk/dist/index.global.js"></script>
<script>
  const vouch = new Vouch.Vouch({
    apiKey: "your-key",
    baseUrl: "https://api.your-domain.com/api/v1",
  });
</script>
```

## Configuration

**Always set `baseUrl` explicitly in production.** The SDK only falls back to a local dev default when omitted.

| Source | Used by | Example |
| --- | --- | --- |
| `VouchConfig.baseUrl` | All runtimes | `https://sandbox.lk.vouch.finance/api/v1` |
| `VOUCH_BASE_URL` env | Node.js / tests | `http://localhost:9393/api/v1` |
| `DEFAULT_DEV_BASE_URL` | Dev fallback only | `http://localhost:9393/api/v1` |

```ts
import { Vouch, DEFAULT_DEV_BASE_URL } from "@vouch.finance/sdk";

const vouch = new Vouch({
  apiKey: process.env.VOUCH_API_KEY!,
  baseUrl: process.env.VOUCH_BASE_URL ?? DEFAULT_DEV_BASE_URL,
  showVoucherOnlyWithBalance: true, // optional: gate UI on spendable balance
});
```

### Browser apps (Vite / React)

Avoid CORS and hardcoded hosts by proxying `/api` to your backend and using a relative SDK base URL:

```env
# .env.local
VITE_BACKEND_URL=http://localhost:9393
VITE_VOUCH_BASE_URL=/api/v1
VITE_VOUCH_API_KEY=sk_test_...
```

See `examples/partner-test-ui/` for a full working setup.

### E2E tests

```bash
cd sdk
VOUCH_API_KEY=sk_test_... \
VOUCH_BASE_URL=http://localhost:9393/api/v1 \
VOUCH_PROGRAM_ID=cmpo31oud0000vzmopcrfb9kx \
VOUCH_BENEFICIARY_PHONE=9953531809 \
npm run test:e2e
```

## Quick Start

```ts
import { Vouch } from "@vouch.finance/sdk";

const vouch = new Vouch({
  apiKey: "sk_test_...",
  baseUrl: "https://api.your-domain.com/api/v1",
});
```

## Voucher balance gate (partner apps)

Before showing voucher UI, check whether the beneficiary has spendable balance:

```ts
const balance = await vouch.vouchers.getBalance({
  beneficiaryPhone: "9953531809",
  programId: "cmpo31oud0000vzmopcrfb9kx",
});

// balance.hasVoucher, balance.hasBalance, balance.remainingBalanceMinor, balance.tokenId

const showUi = await vouch.vouchers.shouldShowUI({
  beneficiaryPhone: "9953531809",
  programId: "cmpo31oud0000vzmopcrfb9kx",
});
// true only when showVoucherOnlyWithBalance is set and hasBalance is true
```

## UPI Voucher Payment Flow

The primary integration for partners building beneficiary-facing apps. The partner identifies the beneficiary by phone number, scans a merchant UPI QR, and completes payment using the voucher.

### Step 1 — Scan merchant UPI QR

```ts
import { parseUPIScheme } from "@vouch.finance/sdk";

const upi = parseUPIScheme("upi://pay?pa=pharmacy@upi&pn=City+Pharmacy&am=250.00");
if (!upi) throw new Error("Not a valid UPI QR");

// upi = {
//   type: "upi",
//   upiId: "pharmacy@upi",
//   merchantName: "City Pharmacy",
//   amount: "250.00",
//   mcc: "5912",
//   raw: "upi://pay?..."
// }
```

Or use the scanner components (see QR Scanner section below).

### Step 2 — Create intent and get quote

```ts
const intent = await vouch.payments.initiateUPI({
  upiData: upi,
  beneficiaryPhone: "+919876543210",
  programId: "cmpo31oud0000vzmopcrfb9kx",
});
```

### Step 3 — Show payment review

Display the quote to the beneficiary. Proceed only if `status === "QUOTED"` and `eligibleAmount > 0`.

### Step 4 — Authorize payment

```ts
const auth = await vouch.payments.authorize({ intentId: intent.intentId });
```

### Step 5 — Poll for settlement

```ts
const status = await vouch.payments.getIntent(intent.intentId);
// status.status — "AUTHORIZED" | "SETTLED" | "FAILED"
```

## Open-Network UPI (Unregistered Merchants)

Payments work to **any** UPI merchant — registered or not. Unregistered merchants return `merchantResolution: "open_network"` and settle via fiat UPI (`settlementRail: "fiat"`).

```ts
const intent = await vouch.payments.initiateUPI({
  upiData: upi,
  beneficiaryPhone: "+919876543210",
  programId: "cmpo31oud0000vzmopcrfb9kx",
});

if (intent.merchantResolution === "open_network") {
  console.log("Paying via UPI to:", intent.upiId);
}
```

## External Settlement (partner-managed money movement)

Use this when **your platform moves the money itself** (e.g. you run your own UPI payout
rail) and you only want Vouch to track the voucher ledger. Pass `externalSettlement: true`
on `initiateUPI`. For the open-network UPI (fiat) settlement path, Vouch will then:

- record the voucher ledger entry and mark the payment `SETTLED`, but
- **not** call the UPI rail engine to perform or confirm the payout.

Optionally pass your own `payoutReference` to store your transfer ID against the payment.

> Scope: this only affects the fiat (open-network) settlement path. On-chain settlement for
> registered merchants is unaffected.

```ts
const intent = await vouch.payments.initiateUPI({
  upiData: upi,
  beneficiaryPhone: "+919876543210",
  programId: "cmpo31oud0000vzmopcrfb9kx",
  externalSettlement: true,            // skip Vouch's UPI payout; you settle the money
  payoutReference: "PARTNER-TXN-001",  // optional: your own transfer reference
});

// intent.externalSettlement === true
// intent.settlementRail === "fiat" for open-network merchants

await vouch.payments.authorize({ intentId: intent.intentId });

// Poll until settled — completes without invoking the UPI rail engine.
const status = await vouch.payments.getIntent(intent.intentId);
// status.status === "SETTLED"
```

## QR Scanner

### React

```tsx
import { QRScanner } from "@vouch.finance/sdk/react";

<QRScanner
  onScan={(upi) => console.log(upi.upiId, upi.amount)}
  onError={(err) => console.error(err)}
  onClose={() => setOpen(false)}
  width={320}
  height={320}
/>
```

### Vanilla JS (any framework)

```ts
import { UPIScanner } from "@vouch.finance/sdk";

const scanner = new UPIScanner({
  container: "#scanner",
  onScan: (upi) => console.log(upi.upiId),
  onError: (err) => console.error(err),
});

await scanner.start();
```

## API Reference

### Client

```ts
new Vouch({
  apiKey,
  baseUrl?,                      // required in production
  version?,                      // default "2026-04-06"
  showVoucherOnlyWithBalance?,   // default false
})
```

All requests include `x-api-key` and `Vouch-Version` headers.

### Programs

| Method | Return |
|--------|--------|
| `vouch.programs.list()` | `{ programs: Program[] }` |
| `vouch.programs.get(slug)` | `{ program: Program }` |
| `vouch.programs.create(params)` | `{ data: { program }, meta }` |
| `vouch.programs.stats(slug)` | `{ data: { stats }, meta }` |
| `vouch.programs.enrol(slug, { phone, fields? })` | `{ data, meta }` |

### Payments

| Method | Return |
|--------|--------|
| `vouch.payments.initiateUPI({ upiData, programId, beneficiaryPhone?, beneficiaryId?, tokenId?, externalSettlement?, payoutReference? })` | `UPIIntentResponse` |
| `vouch.payments.createIntent({ merchantId, items, programId? })` | `{ data: PaymentIntent, meta }` |
| `vouch.payments.quote({ intentId, tokenId })` | `{ data, meta }` |
| `vouch.payments.authorize({ intentId })` | `AuthorizeResponse` |
| `vouch.payments.getIntent(intentId)` | `IntentStatusResponse` |

### Vouchers & Merchants

| Method | Return |
|--------|--------|
| `vouch.vouchers.verify(token, programId?)` | `{ data: Voucher, meta }` |
| `vouch.vouchers.getBalance({ beneficiaryPhone, programId })` | `VoucherBalanceStatus` |
| `vouch.vouchers.shouldShowUI({ beneficiaryPhone, programId })` | `boolean` |
| `vouch.merchants.list(programId?)` | `{ data: Merchant[], meta, pagination }` |

### Constants

| Export | Description |
|--------|-------------|
| `DEFAULT_DEV_BASE_URL` | `http://localhost:9393/api/v1` — local dev fallback |
| `DEFAULT_DEV_BACKEND_ORIGIN` | `http://localhost:9393` — for Vite proxy config |
| `resolveBaseUrl(explicit?)` | Config → `VOUCH_BASE_URL` → dev default |

## Types

| Type | Description |
|------|-------------|
| `VouchConfig` | `apiKey`, `baseUrl?`, `version?`, `showVoucherOnlyWithBalance?` |
| `VoucherBalanceStatus` | Response from `getBalance` — `hasVoucher`, `hasBalance`, `remainingBalanceMinor`, `tokenId`, `currency`, etc. |
| `UPIData` | Parsed UPI QR (`upiId`, `merchantName?`, `amount?`, `mcc?`) |
| `UPIIntentResponse` | Response from `initiateUPI` — includes `merchantResolution`, `settlementRail`, `externalSettlement` |
| `AuthorizeResponse` | Response from `authorize` |
| `IntentStatusResponse` | Response from `getIntent` |
| `VouchError` | `code`, `statusCode`, `requestId`, `message` |

## Error Handling

```ts
import { VouchError } from "@vouch.finance/sdk";

try {
  await vouch.payments.authorize({ intentId: "bad" });
} catch (err) {
  if (err instanceof VouchError) {
    console.log(err.code, err.statusCode, err.requestId, err.message);
  }
}
```

## Examples

| App | Port | Description |
| --- | --- | --- |
| `examples/partner-test-ui/` | 5182 | Dvara flow: balance gate + UPI scan (recommended) |
| `examples/react-app/` | 5180 | Minimal React + `<QRScanner />` |
| `examples/vanilla/` | — | CDN HTML, no build step |

```bash
cd sdk && npm run build

# Partner test UI
cd examples/partner-test-ui
cp .env.local.example .env.local
npm install && npm run dev

# React example
cd examples/react-app
cp .env.local.example .env.local
npm install && npm run dev
```

## Publishing

```bash
cd sdk
npm install
npm run build
NPM_TOKEN=... ./scripts/publish.sh
```

Build outputs: `dist/index.js` (CJS), `dist/index.mjs` (ESM), `dist/index.global.js` (IIFE/CDN), `dist/react/` (React entry).
