@herberthtk/yo-payments-api 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +379 -28
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 (2026-09-11)
4
+
5
+ ### Features
6
+
7
+ * initial TypeScript port of Yo! Payments PHP library ([d55d4ad](https://github.com/herberthk/yo-payments-api/commit/d55d4ad6d14d3f5f3a0b77315adc033b3759fe88))
8
+ * npm packaging, release-it CI, Next.js usage and server hardening ([b78eda7](https://github.com/herberthk/yo-payments-api/commit/b78eda7f88ea8a12fda753d94a19e52b81c66b78))
9
+
10
+ ### Bug Fixes
11
+
12
+ * let release-it publish via npm trusted publishing (OIDC) ([6f311de](https://github.com/herberthk/yo-payments-api/commit/6f311de2db1fd7b3d586b0b8d33e26417cfee606))
13
+
3
14
  All notable changes to this project are documented here.
4
15
 
5
16
  Releases are managed with [release-it](https://github.com/release-it/release-it)
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @herberthtk/yo-payments-api
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@herberthtk/yo-payments-api.svg)](https://www.npmjs.com/package/@herberthtk/yo-payments-api)
4
+ [![CI](https://github.com/herberthk/yo-payments-api/actions/workflows/ci.yml/badge.svg)](https://github.com/herberthk/yo-payments-api/actions)
5
+ [![license](https://img.shields.io/npm/l/@herberthtk/yo-payments-api.svg)](https://github.com/herberthk/yo-payments-api/blob/main/LICENSE)
6
+
3
7
  TypeScript client for the [Yo! Payments API PHP library](https://github.com/YO-Uganda) (`YoAPI.php`) for mobile money, airtime and account operations on the Yo! Payments gateway. Runs on [Bun](https://bun.com) and Node.js 18+ (uses `fetch` + `node:crypto`), including Next.js App Router handlers, Server Actions and Server Components (**server-side only** — never import it into a Client Component).
4
8
 
5
9
  ## Install
@@ -9,7 +13,7 @@ npm install @herberthtk/yo-payments-api
9
13
  # or: bun add @herberthtk/yo-payments-api
10
14
  ```
11
15
 
12
- ## Usage
16
+ ## Quick start
13
17
 
14
18
  ```ts
15
19
  import { YoAPI } from "@herberthtk/yo-payments-api";
@@ -30,23 +34,379 @@ console.log(balance.balance); // [{ code: "UGX", balance: "50000" }, ...]
30
34
 
31
35
  All network methods are `async` and return typed response objects. Method names use idiomatic camelCase (e.g. `acDepositFunds`, `setExternalReference`, `getTransactionLimitAccountIdentifier`) — the one intentional divergence from the PHP library's `snake_case` names; the XML wire format is unchanged.
32
36
 
33
- ### Available operations
37
+ ## Configuration
38
+
39
+ ```ts
40
+ const yoAPI = new YoAPI(username: string, password: string, mode: "production" | "sandbox" = "production");
41
+ ```
42
+
43
+ | Setter | Type | Default | Purpose |
44
+ |---|---|---|---|
45
+ | `setExternalReference` | `string \| null` | `null` | Your reference for the payment (e.g. invoice number); sent with most requests |
46
+ | `setInternalReference` | `string \| null` | `null` | Reference to another Yo! Payments system transaction |
47
+ | `setNonblocking` | `"TRUE" \| "FALSE"` | `"FALSE"` | `"TRUE"` returns immediately; poll status or use IPN URLs |
48
+ | `setInstantNotificationUrl` | `string \| null` | `null` | URL POSTed on successful deposit (non-blocking flow) |
49
+ | `setFailureNotificationUrl` | `string \| null` | `null` | URL POSTed on failed deposit (non-blocking flow) |
50
+ | `setProviderReferenceText` | `string \| null` | `null` | Text appended to the subscriber's confirmation SMS |
51
+ | `setAuthenticationSignatureBase64` | `string \| null` | `null` | Required for certain deposit requests (ask Yo! support) |
52
+ | `setDepositTransactionType` | `"PULL" \| "PUSH"` | `"PULL"` | Which deposit flow `acTransactionCheckStatus` follows up on |
53
+ | `setTransactionLimitAccountIdentifier` | `string \| null` | `null` | Ask your account administrator before using |
54
+ | `setPublicKeyAuthenticationNonce` | `string \| null` | `null` | Unique-per-request nonce for public-key-auth payouts |
55
+ | `setPublicKeyAuthenticationSignatureBase64` | `string \| null` | `null` | Usually set via `generatePublicKeyAuthenticationSignature` |
56
+ | `setPrivateKeyFileLocation` | `string \| null` | `null` | Path to the signing private key (PEM file) |
57
+ | `setPrivateKeyContent` | `string \| null` | `null` | Key PEM text; for serverless hosts without key files (wins over file location) |
58
+ | `setPublicKeyFileUrl` | `string` | bundled cert | Certificate used to verify IPN signatures |
59
+ | `setUrl` | `string` | gateway URL | Override the API endpoint (testing/proxies) |
60
+ | `setTimeout` | `number` (ms) | `120000` | Request timeout; `<= 0` disables it |
61
+ | `setTlsVerificationEnabled` | `boolean` | `true` | Only disable for testing against self-signed endpoints |
62
+ | `setMaxResponseBytes` | `number` | `1048576` | Cap on gateway response bodies |
63
+
64
+ Every setter has a matching getter (`getExternalReference()`, `getMode()`, …). One instance holds per-request state, so create a fresh client per request — never share one across concurrent operations.
65
+
66
+ ## API reference
67
+
68
+ Conventions used below:
69
+
70
+ - **Success** — `Status: "OK"` (and usually `TransactionStatus: "SUCCEEDED"`); reference fields are present.
71
+ - **Business failure** — returned as a normal object, never thrown: `Status: "FAILED"` with `ErrorMessageCode` / `ErrorMessage` set. Check `Status` (and `TransactionStatus`) before trusting reference fields.
72
+ - **Transport failure** — thrown as `YoAPIError`: connection errors, timeouts, non-2xx HTTP, oversized bodies, malformed XML, missing `<Response>`. See [Error handling](#error-handling).
73
+
74
+ Amounts accept `number | string` — pass a string when exact formatting matters (e.g. `"100.50"`), since numbers use JavaScript float-to-string conversion. Phone numbers use international format without `+` (e.g. `"256770000000"`).
75
+
76
+ ### acDepositFunds — request a mobile money deposit (USSD PIN prompt)
77
+
78
+ ```ts
79
+ const res: DepositFundsResponse = await yoAPI.acDepositFunds(msisdn, amount, narrative);
80
+ ```
81
+
82
+ | Parameter | Type | Description |
83
+ |---|---|---|
84
+ | `msisdn` | `string` | Subscriber phone, e.g. `"256770000000"` |
85
+ | `amount` | `number \| string` | Amount to collect |
86
+ | `narrative` | `string` | Reason shown to the subscriber |
34
87
 
35
- - `acDepositFunds(msisdn, amount, narrative)`
36
- - `acTransactionCheckStatus(transactionReference, privateTransactionReference?)`
37
- - `acInternalTransfer(currencyCode, amount, beneficiaryAccount, beneficiaryEmail, narrative)`
38
- - `acAcctBalance()`
39
- - `acGetMinistatement(startDate?, endDate?, transactionStatus?, currencyCode?, resultSetLimit?, transactionEntryDesignation?, externalReference?)`
40
- - `acSendAirtimeMobile(msisdn, amount, narrative)`
41
- - `acSendAirtimeInternal(currencyCode, amount, beneficiaryAccount, beneficiaryEmail, narrative)`
42
- - `acWithdrawFunds(msisdn, amount, narrative)`
43
- - `acUserPurchaseAirtimestock(airtimeCurrencyCode, amount)`
44
- - `acGetMsisdnKycInfo(msisdn)`
45
- - `generatePublicKeyAuthenticationSignature(msisdn, amount, narrative)`
88
+ Response (`DepositFundsResponse`): `Status`, `StatusCode`, `StatusMessage`, `TransactionStatus` always present. On success also `TransactionReference` (save this — it identifies the payment everywhere else), `MNOTransactionReferenceId`, `IssuedReceiptNumber`. On business failure, `ErrorMessageCode` / `ErrorMessage` instead. Optional request tweaks: `setNonblocking("TRUE")` + IPN URLs, `setAuthenticationSignatureBase64`.
89
+
90
+ ### acTransactionCheckStatus poll a transaction
91
+
92
+ ```ts
93
+ const res: TransactionCheckStatusResponse = await yoAPI.acTransactionCheckStatus(
94
+ transactionReference: string | null,
95
+ privateTransactionReference: string | null = null,
96
+ );
97
+ ```
98
+
99
+ Pass the gateway `TransactionReference`, or `null` plus the `ExternalReference` you sent (`privateTransactionReference`). `setDepositTransactionType("PUSH")` first when following up a push deposit. Same base fields as deposits, plus (when available): `Amount`, `AmountFormatted`, `CurrencyCode`, `TransactionInitiationDate`, `TransactionCompletionDate`. `TransactionStatus` is one of `SUCCEEDED`, `PENDING`, `FAILED`, `INDETERMINATE` — poll until it leaves `PENDING`.
100
+
101
+ ### acInternalTransfer — pay another Yo! Payments account
102
+
103
+ ```ts
104
+ const res: DepositFundsResponse = await yoAPI.acInternalTransfer(
105
+ currencyCode: string, // e.g. "UGX-MTNMM", "UGX-MTNAT", "UGX-WTLAT", "UGX-OULAT", "UGX-AIRAT"
106
+ amount: number | string,
107
+ beneficiaryAccount: number | string, // recipient Yo! account number
108
+ beneficiaryEmail: string,
109
+ narrative: string,
110
+ );
111
+ ```
112
+
113
+ Same response shape as deposits (success/failure fields as above).
114
+
115
+ ### acAcctBalance — account balances
116
+
117
+ ```ts
118
+ const res: AcctBalanceResponse = await yoAPI.acAcctBalance();
119
+ // res.balance → [{ code: "UGX", balance: "50000" }, { code: "UGX-MTNAT", balance: "1500" }, ...]
120
+ ```
121
+
122
+ `Status` / `StatusCode` always present, `balance` always an array (possibly empty), plus optional `StatusMessage` / error fields.
123
+
124
+ ### acGetMinistatement — transaction history
125
+
126
+ ```ts
127
+ const res: MinistatementResponse = await yoAPI.acGetMinistatement(
128
+ startDate: string | null = null, // "YYYY-MM-DD HH:MM:SS"
129
+ endDate: string | null = null, // "YYYY-MM-DD HH:MM:SS"
130
+ transactionStatus: string | null = null, // "SUCCEEDED", "FAILED", "PENDING", "INDETERMINATE", or comma-joined
131
+ currencyCode: string | null = null, // e.g. "UGX-MTNMM", "UGX-WARIDMM"
132
+ resultSetLimit: number | null = null, // 0 returns all; gateway default is 15
133
+ transactionEntryDesignation = "ANY", // "TRANSACTION" | "CHARGES" | "ANY"
134
+ externalReference: string | null = null,
135
+ );
136
+ ```
137
+
138
+ `Status`, `StatusCode`, `TotalTransactions`, `ReturnedTransactions` and `Transactions` always present. Each `TransactionDetail` carries `TransactionSystemId`, `TransactionReference`, `TransactionStatus`, `InitiationDate`, `CompletionDate`, `NarrativeBase64`, `Currency`, `Amount`, `Balance`, `GeneralType`, `DetailedType`, `BeneficiaryBase64`, `SenderBase64`, `TransactionEntryDesignation`, plus optional `BeneficiaryMsisdn`, `SenderMsisdn`, `Base64TransactionExternalReference` (present only when the gateway sends them).
139
+
140
+ ### acSendAirtimeMobile / acSendAirtimeInternal — send airtime
141
+
142
+ ```ts
143
+ // to a phone number
144
+ await yoAPI.acSendAirtimeMobile(msisdn, amount, narrative);
145
+ // to another Yo! account ("UGX-MTNAT" | "UGX-WTLAT" | "UGX-OULAT" | "UGX-AIRAT")
146
+ await yoAPI.acSendAirtimeInternal(currencyCode, amount, beneficiaryAccount, beneficiaryEmail, narrative);
147
+ ```
148
+
149
+ Same response shape as deposits.
150
+
151
+ ### acWithdrawFunds — pay out to mobile money (handle with care)
152
+
153
+ ```ts
154
+ const res: DepositFundsResponse = await yoAPI.acWithdrawFunds(msisdn, amount, narrative);
155
+ ```
156
+
157
+ Same response shape as deposits. Requires an API Access Letter; some payouts additionally require public-key authentication — see below. Optional: `setTransactionLimitAccountIdentifier`, `setPublicKeyAuthenticationNonce` + `setPublicKeyAuthenticationSignatureBase64`.
158
+
159
+ ### acUserPurchaseAirtimestock — buy airtime stock with mobile money credit
160
+
161
+ ```ts
162
+ const res: PurchaseAirtimeStockResponse = await yoAPI.acUserPurchaseAirtimestock(
163
+ airtimeCurrencyCode: string, // "UGX-MTNAT" | "UGX-AIRAT" | "UGX-OULAT" | "UGX-UTLAT" | "UGX-SMTAT"
164
+ amount: number | string,
165
+ );
166
+ ```
167
+
168
+ `Status` / `StatusCode` always present; on success `TransactionReference`, `TotalCurrencyDebited`, `CommissionAmount`, `StatusMessage`. (Parity note: your external reference is sent inside a `<TransactionReference>` tag, exactly like the PHP library.)
169
+
170
+ ### acGetMsisdnKycInfo — name lookup before paying out
171
+
172
+ ```ts
173
+ const res: MsisdnKycInfoResponse = await yoAPI.acGetMsisdnKycInfo("256770000000");
174
+ // res.FirstName / res.MiddleName / res.Surname when the gateway returns them
175
+ ```
176
+
177
+ MTN Uganda and Airtel Uganda only; needs permission from support@yo.co.ug. `Status` / `StatusCode` always present.
178
+
179
+ ### receivePaymentNotification / receivePaymentFailureNotification — verify IPNs
180
+
181
+ ```ts
182
+ const payment: PaymentNotificationResult = yoAPI.receivePaymentNotification({
183
+ date_time, amount, narrative, network_ref, external_ref, msisdn, signature,
184
+ });
185
+ // payment.is_verified === true → trust payment.msisdn / .amount / .external_ref / ...
186
+ const failure: PaymentFailureNotificationResult = yoAPI.receivePaymentFailureNotification({
187
+ failed_transaction_reference, transaction_init_date, verification,
188
+ });
189
+ ```
190
+
191
+ Pass the parsed POST form body (PHP reads `$_POST`; here you supply it). Verification is RSA-SHA256 against the bundled Yo! certificate and is fail-closed: any problem (bad signature, missing cert) yields `is_verified: false`, never a throw. Always gate crediting on `is_verified` **and** dedupe on `external_ref` — notifications carry no replay protection.
192
+
193
+ ### generatePublicKeyAuthenticationSignature — sign a payout
194
+
195
+ ```ts
196
+ yoAPI.setExternalReference("INV-123");
197
+ yoAPI.setPublicKeyAuthenticationNonce(crypto.randomUUID()); // unique per request
198
+ yoAPI.setPrivateKeyContent(process.env.YO_PRIVATE_KEY!.replace(/\\n/g, "\n")); // or setPrivateKeyFileLocation(path)
199
+ yoAPI.generatePublicKeyAuthenticationSignature(msisdn, amount, narrative); // throws on missing/invalid key
200
+ const res = await yoAPI.acWithdrawFunds(msisdn, amount, narrative);
201
+ ```
202
+
203
+ Signs `username + amount + msisdn + narrative + externalReference + nonce` (SHA1+RSA per the gateway protocol) and stores it for the next payout call. Throws `"Public key authentication nonce is not set…"`, `"Private key file location cannot be NULL"`, `"Private key file could not be opened…"`, or `"Private key is invalid"`.
204
+
205
+ ### Response examples
206
+
207
+ Concrete objects each call resolves to. Absent optional fields are omitted (never `null`).
208
+
209
+ **Deposits, transfers, airtime, withdrawals** (`DepositFundsResponse` family) — success:
210
+ ```ts
211
+ {
212
+ Status: "OK",
213
+ StatusCode: "200",
214
+ StatusMessage: "OK",
215
+ TransactionStatus: "SUCCEEDED",
216
+ TransactionReference: "TRX-EX-1",
217
+ MNOTransactionReferenceId: "MNO-9",
218
+ IssuedReceiptNumber: "R-77",
219
+ }
220
+ ```
221
+
222
+ Same calls — business failure (returned, not thrown):
223
+ ```ts
224
+ {
225
+ Status: "FAILED",
226
+ StatusCode: "500",
227
+ StatusMessage: "Failed",
228
+ TransactionStatus: "FAILED",
229
+ ErrorMessageCode: "INVALID_MSISDN",
230
+ ErrorMessage: "The MSISDN is invalid",
231
+ }
232
+ ```
233
+
234
+ **Transaction status** (`TransactionCheckStatusResponse`) — success carries the money fields:
235
+ ```ts
236
+ {
237
+ Status: "OK",
238
+ StatusCode: "200",
239
+ StatusMessage: "OK",
240
+ TransactionStatus: "SUCCEEDED",
241
+ TransactionReference: "TRX-EX-1",
242
+ Amount: "10000",
243
+ AmountFormatted: "UGX 10,000",
244
+ CurrencyCode: "UGX",
245
+ TransactionInitiationDate: "2026-09-07T10:00:00",
246
+ TransactionCompletionDate: "2026-09-07T10:01:00",
247
+ IssuedReceiptNumber: "R-77",
248
+ }
249
+ ```
250
+
251
+ Still pending — keep polling:
252
+ ```ts
253
+ {
254
+ Status: "OK",
255
+ StatusCode: "200",
256
+ StatusMessage: "OK",
257
+ TransactionStatus: "PENDING",
258
+ }
259
+ ```
260
+
261
+ **Balance** (`AcctBalanceResponse`):
262
+ ```ts
263
+ {
264
+ Status: "OK",
265
+ StatusCode: "200",
266
+ balance: [
267
+ { code: "UGX", balance: "50000" },
268
+ { code: "UGX-MTNAT", balance: "1500" },
269
+ ],
270
+ }
271
+ ```
272
+
273
+ **Ministatement** (`MinistatementResponse`) — `Transactions` is always an array:
274
+ ```ts
275
+ {
276
+ Status: "OK",
277
+ StatusCode: "200",
278
+ TotalTransactions: "2",
279
+ ReturnedTransactions: "2",
280
+ Transactions: [
281
+ {
282
+ TransactionSystemId: "SYS-1",
283
+ TransactionReference: "TRX-EX-1",
284
+ TransactionStatus: "SUCCEEDED",
285
+ InitiationDate: "2026-09-07 10:00:00",
286
+ CompletionDate: "2026-09-07 10:01:00",
287
+ NarrativeBase64: "SGVsbG8=",
288
+ Currency: "UGX",
289
+ Amount: "100",
290
+ Balance: "900",
291
+ GeneralType: "DEPOSIT",
292
+ DetailedType: "MOBILE_MONEY_DEPOSIT",
293
+ BeneficiaryMsisdn: "256770000000",
294
+ BeneficiaryBase64: "QmVuZQ==",
295
+ SenderMsisdn: "256780000000",
296
+ SenderBase64: "U2VuZGVy",
297
+ Base64TransactionExternalReference: "RVhULTE=",
298
+ TransactionEntryDesignation: "TRANSACTION",
299
+ },
300
+ ],
301
+ }
302
+ ```
303
+
304
+ **Airtimestock purchase** (`PurchaseAirtimeStockResponse`):
305
+ ```ts
306
+ {
307
+ Status: "OK",
308
+ StatusCode: "200",
309
+ StatusMessage: "Purchased",
310
+ TransactionReference: "TRX-EX-1",
311
+ TotalCurrencyDebited: "1000",
312
+ CommissionAmount: "50",
313
+ }
314
+ ```
315
+
316
+ **KYC lookup** (`MsisdnKycInfoResponse`):
317
+ ```ts
318
+ {
319
+ Status: "OK",
320
+ StatusCode: "200",
321
+ StatusMessage: "Found",
322
+ FirstName: "John",
323
+ MiddleName: "Middle",
324
+ Surname: "Doe",
325
+ }
326
+ ```
327
+
328
+ **Verified payment notification** (`PaymentNotificationResult`):
329
+ ```ts
330
+ {
331
+ is_verified: true,
332
+ date_time: "2026-09-07 10:00:00",
333
+ amount: "1000",
334
+ narrative: "Payment",
335
+ network_ref: "NET-1",
336
+ external_ref: "EXT-1",
337
+ msisdn: "256770000000",
338
+ }
339
+ ```
340
+
341
+ Unverifiable notification (bad signature or cert problem) — credit nothing:
342
+ ```ts
343
+ {
344
+ is_verified: false,
345
+ date_time: "2026-09-07 10:00:00",
346
+ amount: "9999",
347
+ narrative: "Payment",
348
+ network_ref: "NET-1",
349
+ external_ref: "EXT-1",
350
+ msisdn: "256770000000",
351
+ }
352
+ ```
353
+
354
+ **Transport failure** — thrown as `YoAPIError`, e.g. gateway HTTP 502:
355
+ ```ts
356
+ // caught error instance:
357
+ YoAPIError: Yo! Payments gateway responded with HTTP 502
358
+ // e.status === 502
359
+ // e.body === "<html><body>Bad Gateway</body></html>"
360
+ // e.cause === undefined (set only for connection/timeout errors)
361
+ ```
362
+
363
+ ## Usage cases
364
+
365
+ **1. Blocking deposit** — simplest collection flow; the call returns after the subscriber approves:
366
+ ```ts
367
+ const api = new YoAPI(u, p, "sandbox");
368
+ api.setExternalReference(`INV-${Date.now()}`);
369
+ const res = await api.acDepositFunds("256770000000", 10000, "Order payment");
370
+ if (res.Status === "OK" && res.TransactionStatus === "SUCCEEDED") {
371
+ await markPaid(res.TransactionReference!);
372
+ } else {
373
+ console.error(res.ErrorMessageCode, res.ErrorMessage);
374
+ }
375
+ ```
376
+
377
+ **2. Non-blocking deposit with IPN + polling fallback** — instant response, then confirm:
378
+ ```ts
379
+ api.setNonblocking("TRUE");
380
+ api.setInstantNotificationUrl("https://example.com/api/yo/ipn");
381
+ api.setFailureNotificationUrl("https://example.com/api/yo/failure");
382
+ const res = await api.acDepositFunds("256770000000", 10000, "Order payment");
383
+ // ...meanwhile your IPN endpoint verifies and credits on payment.external_ref...
384
+ // ...and/or poll until settled:
385
+ for (;;) {
386
+ const st = await api.acTransactionCheckStatus(null, externalRef);
387
+ if (st.TransactionStatus !== "PENDING") break;
388
+ await new Promise((r) => setTimeout(r, 5000));
389
+ }
390
+ ```
391
+
392
+ **3. Daily reconciliation from the ministatement:**
393
+ ```ts
394
+ const st = await api.acGetMinistatement("2026-09-10 00:00:00", "2026-09-10 23:59:59", "SUCCEEDED", "UGX-MTNMM", 0);
395
+ for (const tx of st.Transactions) await reconcile(tx);
396
+ ```
397
+
398
+ **4. Serverless payout with key material (no key files on Vercel/Lambda):**
399
+ ```ts
400
+ api.setExternalReference("SAL-SEP-001");
401
+ api.setPublicKeyAuthenticationNonce(crypto.randomUUID());
402
+ api.setPrivateKeyContent(process.env.YO_PRIVATE_KEY!.replace(/\\n/g, "\n"));
403
+ api.generatePublicKeyAuthenticationSignature("256770000000", 5000, "Payout");
404
+ const res = await api.acWithdrawFunds("256770000000", 5000, "Payout");
405
+ ```
46
406
 
47
407
  ### Receiving payment notifications (IPN)
48
408
 
49
- PHP reads `$_POST` / `php://input` globals, which is impossible in TypeScript, so you pass the parsed form body yourself. Point `setUrl`-style config is not affected; use `setPublicKeyFileUrl` if you need a different certificate (sandbox vs production is picked automatically by the constructor `mode`).
409
+ PHP reads `$_POST` / `php://input` globals, which is impossible in TypeScript, so you pass the parsed form body yourself. Use `setPublicKeyFileUrl` if you need a different certificate (sandbox vs production is picked automatically by the constructor `mode`).
50
410
 
51
411
  ```ts
52
412
  // Bun HTTP server example
@@ -70,17 +430,6 @@ Bun.serve({
70
430
  });
71
431
  ```
72
432
 
73
- ### Public key authentication (payouts)
74
-
75
- ```ts
76
- const yoAPI = new YoAPI("API_USERNAME", "API_PASSWORD");
77
- yoAPI.setExternalReference("INV-123");
78
- yoAPI.setPublicKeyAuthenticationNonce(crypto.randomUUID());
79
- yoAPI.setPrivateKeyFileLocation("/path/to/your-private-key.pem");
80
- yoAPI.generatePublicKeyAuthenticationSignature("256770000000", 5000, "Salary payout");
81
- const res = await yoAPI.acWithdrawFunds("256770000000", 5000, "Salary payout");
82
- ```
83
-
84
433
  ### Usage in Next.js (App Router)
85
434
 
86
435
  The library is **server-only**: it uses `node:crypto`/`node:fs` and handles API secrets. Add `import "server-only"` (`npm i server-only`) at the top of every file that touches it, keep credentials in server-side env vars (never `NEXT_PUBLIC_*`), and pin `export const runtime = "nodejs"` on route handlers. Ready-to-copy handlers live in `examples/nextjs/`:
@@ -170,16 +519,18 @@ try {
170
519
  }
171
520
  ```
172
521
 
173
- `YoAPIError` is thrown for connection errors, timeouts, non-2xx HTTP statuses, oversized bodies, malformed XML and responses missing the `<Response>` node. Gateway-level business failures (e.g. `Status: "FAILED"`) are still returned as normal response objects, exactly like the PHP library.
522
+ `YoAPIError` fields: `message` (what failed), `status?: number` (HTTP status when a response was received), `body?: string` (first 500 chars of the response, when any), `cause?: unknown` (the underlying fetch error). Thrown for connection errors, timeouts, non-2xx HTTP statuses, oversized bodies, malformed XML and responses missing the `<Response>` node. Gateway-level business failures (e.g. `Status: "FAILED"`) are still returned as normal response objects, exactly like the PHP library.
174
523
 
175
524
  ### Examples
176
525
 
177
- The `examples/` directory ports all six PHP examples; each exports testable functions and is runnable with `bun run`:
526
+ The `examples/` directory ports the PHP examples (plus balance and KYC extras); each exports testable functions and is runnable with `bun run`:
178
527
 
179
528
  ```bash
180
529
  YO_API_USERNAME=... YO_API_PASSWORD=... YO_API_MODE=sandbox bun run examples/deposit_funds.ts
181
530
  YO_API_USERNAME=... YO_API_PASSWORD=... YO_API_MODE=sandbox bun run examples/deposit_funds_nonblocking.ts
182
531
  YO_API_USERNAME=... YO_API_PASSWORD=... YO_API_MODE=sandbox bun run examples/get_ministatement.ts
532
+ YO_API_USERNAME=... YO_API_PASSWORD=... YO_API_MODE=sandbox bun run examples/get_account_balance.ts
533
+ YO_API_USERNAME=... YO_API_PASSWORD=... YO_API_MODE=sandbox bun run examples/get_user_info.ts
183
534
  YO_API_USERNAME=... YO_API_PASSWORD=... YO_API_MODE=sandbox \
184
535
  YO_PRIVATE_KEY_FILE=/path/to/private-key.pem \
185
536
  bun run examples/withdraw_funds_public_key_authentication.ts
@@ -227,7 +578,7 @@ Versions follow [Conventional Commits](https://www.conventionalcommits.org/) (`f
227
578
  - `src/keys.ts` — cached verification-key loading (file-first, embedded fallback)
228
579
  - `src/constants.ts` — gateway URLs, certificate names, defaults
229
580
  - `src/embeddedCerts.ts` — auto-generated from `certs/` (`bun run embed-certs`)
230
- - `examples/` — runnable ports of the six PHP examples
581
+ - `examples/` — runnable ports of the PHP examples (plus balance and KYC extras)
231
582
  - `examples/nextjs/` — Next.js App Router handlers, Server Actions and queries
232
583
  - `certs/` — Yo! Uganda public certificates for IPN verification
233
584
  - `.github/workflows/` — `ci.yml` (test/typecheck/build/pack-check) and `release.yml` (release-it via trusted publishing)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herberthtk/yo-payments-api",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "TypeScript client for the Yo! Payments mobile-money gateway (deposits, withdrawals, airtime, statements, IPN verification). Works in Node.js 18+, Bun, and Next.js (server-side).",
5
5
  "license": "MIT",
6
6
  "repository": {