@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.
- package/CHANGELOG.md +11 -0
- package/README.md +379 -28
- 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
|
+
[](https://www.npmjs.com/package/@herberthtk/yo-payments-api)
|
|
4
|
+
[](https://github.com/herberthk/yo-payments-api/actions)
|
|
5
|
+
[](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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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.
|
|
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`
|
|
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
|
|
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
|
|
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.
|
|
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": {
|