@nombaoss/nomba-node 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nomba
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,468 @@
1
- # Temporary Holding Version
1
+ # nomba-node
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ TypeScript and Node.js SDK for the [Nomba](https://nomba.com) payments API.
4
+
5
+ [![CI](https://github.com/NombaOSS/nomba-node/actions/workflows/ci.yml/badge.svg)](https://github.com/NombaOSS/nomba-node/actions/workflows/ci.yml)
6
+ [![npm](https://img.shields.io/npm/v/@nombaoss/nomba-node)](https://www.npmjs.com/package/@nombaoss/nomba-node)
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+
9
+ - Zero runtime dependencies. Uses the global `fetch`. Node 18 or later.
10
+ - ESM and CommonJS, with TypeScript types for every request and response.
11
+ - Handles authentication, token refresh, safe retries, idempotency references, pagination and webhook verification.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @nombaoss/nomba-node
17
+ ```
18
+
19
+ ## Quickstart
20
+
21
+ ```ts
22
+ import Nomba from "@nombaoss/nomba-node";
23
+
24
+ const nomba = new Nomba({
25
+ clientId: process.env.NOMBA_CLIENT_ID,
26
+ clientSecret: process.env.NOMBA_CLIENT_SECRET,
27
+ accountId: process.env.NOMBA_ACCOUNT_ID, // your parent account ID
28
+ }); // sandbox by default
29
+
30
+ const balance = await nomba.accounts.getBalance();
31
+ console.log(balance.amount, balance.currency);
32
+ ```
33
+
34
+ CommonJS: `const { Nomba } = require("@nombaoss/nomba-node");`
35
+
36
+ Get sandbox credentials from your Nomba dashboard. More examples are in [`examples/`](examples).
37
+
38
+ ## Configuration
39
+
40
+ | Option | Default | Notes |
41
+ | -------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------- |
42
+ | `clientId` | env `NOMBA_CLIENT_ID` | Required. |
43
+ | `clientSecret` | env `NOMBA_CLIENT_SECRET` | Required. |
44
+ | `accountId` | env `NOMBA_ACCOUNT_ID` | Required. Parent account ID, sent as the `accountId` header on every request. |
45
+ | `environment` | env `NOMBA_ENVIRONMENT`, else `"sandbox"` | `"sandbox"` (`https://sandbox.nomba.com`) or `"production"` (`https://api.nomba.com`). |
46
+ | `baseUrl` | from `environment` | Override for proxies and tests. Must be `https://`, except for `localhost` and `127.0.0.1`. |
47
+ | `timeout` | `30000` | Per-request timeout in milliseconds. |
48
+ | `maxRetries` | `2` | Automatic retries for GET and token requests. See [Retries](#retries). |
49
+ | `tokenStore` | in memory | See [Token store](#token-store). |
50
+ | `fetch` | global `fetch` | Custom `fetch` implementation. |
51
+
52
+ A missing credential throws a `ConfigurationError` at construction that names the option and the environment variable. The constructor makes no network calls; the first API call gets the access token.
53
+
54
+ Every method returns the response's `data` field. The `{ code, description, data }` envelope is unwrapped for you.
55
+
56
+ ## Resources
57
+
58
+ ### Accounts
59
+
60
+ ```ts
61
+ const parent = await nomba.accounts.getParent();
62
+ const balance = await nomba.accounts.getBalance(); // parent account
63
+ const subBalance = await nomba.accounts.getBalance({ subAccountId: "..." });
64
+ const sub = await nomba.accounts.getSubAccount({ accountRef: "store-12" });
65
+ const page = await nomba.accounts.listTerminals({ limit: 20 });
66
+ ```
67
+
68
+ Methods that act on a sub account take an optional `subAccountId` instead of having separate methods.
69
+
70
+ ### Virtual accounts
71
+
72
+ ```ts
73
+ const va = await nomba.virtualAccounts.create({
74
+ accountRef: "order-881",
75
+ accountName: "Ada Obi",
76
+ });
77
+ await nomba.virtualAccounts.get("order-881");
78
+ await nomba.virtualAccounts.update("order-881", { accountName: "Ada N. Obi" });
79
+ await nomba.virtualAccounts.expire("order-881");
80
+ const page = await nomba.virtualAccounts.list({ expired: false }, { limit: 50 });
81
+ ```
82
+
83
+ ### Checkout
84
+
85
+ ```ts
86
+ const { checkoutLink, orderReference } = await nomba.checkout.createOrder({
87
+ order: {
88
+ amount: 5000,
89
+ currency: "NGN",
90
+ customerEmail: "customer@example.com",
91
+ callbackUrl: "https://example.com/checkout/done",
92
+ },
93
+ tokenizeCard: true,
94
+ });
95
+
96
+ const tx = await nomba.checkout.getTransaction({
97
+ idType: "ORDER_REFERENCE",
98
+ id: orderReference,
99
+ });
100
+
101
+ // Later: charge the saved card using the token from the payment_success webhook.
102
+ const charge = await nomba.checkout.chargeSavedCard({
103
+ order: {
104
+ amount: 5000,
105
+ currency: "NGN",
106
+ customerEmail: "customer@example.com",
107
+ callbackUrl: "https://example.com/cb",
108
+ },
109
+ tokenKey: "token-from-webhook",
110
+ });
111
+ if (charge.otpRequired) {
112
+ await nomba.checkout.submitCardOtp({
113
+ otp: "123456",
114
+ orderReference: charge.orderReference!,
115
+ transactionId: charge.transactionId!,
116
+ });
117
+ }
118
+ ```
119
+
120
+ Also: `getOrder`, `cancelOrder`, `refund`, `listSavedCards`, `updateSavedCardEmail`, `deleteSavedCard`, `getTransferAccount`, `submitCardDetails`, `resendOtp`, `confirmTransactionReceipt`, `cancelTransaction`, `requestSaveCardOtp`, `requestSavedCardsOtp`, `submitUserOtp` and `getSavedCards`.
121
+
122
+ ### Transfers
123
+
124
+ ```ts
125
+ const banks = await nomba.transfers.listBanks();
126
+ const { accountName } = await nomba.transfers.lookupAccount({
127
+ accountNumber: "0123456789",
128
+ bankCode: "058",
129
+ });
130
+
131
+ const transfer = await nomba.transfers.toBank({
132
+ amount: 1000,
133
+ accountNumber: "0123456789",
134
+ accountName: accountName!,
135
+ bankCode: "058",
136
+ narration: "Invoice 42",
137
+ merchantTxRef: "invoice-42-payout", // your idempotency reference
138
+ });
139
+
140
+ await nomba.transfers.toWallet({ amount: 500, receiverAccountId: "..." });
141
+ ```
142
+
143
+ ### Transactions
144
+
145
+ ```ts
146
+ const page = await nomba.transactions.list({
147
+ limit: 50,
148
+ dateFrom: "2026-01-01T00:00:00",
149
+ });
150
+ const filtered = await nomba.transactions.filter({ status: "SUCCESS" }, { limit: 20 });
151
+ const tx = await nomba.transactions.get({ merchantTxRef: "invoice-42-payout" });
152
+ const credits = await nomba.transactions.listBankTransactions();
153
+ const bySession = await nomba.transactions.requery("100004250929105143781234567890");
154
+ ```
155
+
156
+ `transactions.get` needs exactly one of `transactionRef`, `merchantTxRef`, `orderReference` or `orderId`.
157
+
158
+ ### Terminals
159
+
160
+ ```ts
161
+ await nomba.terminals.assign({ serialNumber: "SN123", terminalLabel: "Till 1" });
162
+ await nomba.terminals.unassign({ serialNumber: "SN123" });
163
+ await nomba.terminals.sendPaymentRequest("2KUD0001", {
164
+ merchantTxRef: "till-1-0042",
165
+ amount: 2500,
166
+ currency: "NGN",
167
+ });
168
+ ```
169
+
170
+ ### Direct debits
171
+
172
+ ```ts
173
+ const mandate = await nomba.directDebits.createMandate({
174
+ /* customer, amount, frequency, dates */ ...mandateDetails,
175
+ });
176
+ await nomba.directDebits.getMandateStatus(mandate.mandateId);
177
+ await nomba.directDebits.debitMandate({ mandateId: mandate.mandateId, amount: "5000" });
178
+ await nomba.directDebits.listMandates({ page: 1, pageSize: 20 });
179
+ await nomba.directDebits.updateMandateStatus({
180
+ mandateId: mandate.mandateId,
181
+ status: "SUSPEND",
182
+ });
183
+ ```
184
+
185
+ ### Bills
186
+
187
+ ```ts
188
+ await nomba.bills.buyAirtime({ amount: 500, phoneNumber: "08012345678", network: "MTN" });
189
+ const plans = await nomba.bills.listDataPlans("mtn");
190
+ await nomba.bills.buyData({
191
+ productId: plans[0]!.productId!,
192
+ phoneNumber: "08012345678",
193
+ network: "MTN",
194
+ });
195
+
196
+ const customer = await nomba.bills.lookupElectricityCustomer({
197
+ disco: "IKEDC",
198
+ customerId: "45012345678",
199
+ });
200
+ await nomba.bills.buyElectricity({
201
+ disco: "IKEDC",
202
+ customerId: "45012345678",
203
+ amount: 5000,
204
+ meterType: "PREPAID",
205
+ payerName: customer.name,
206
+ });
207
+
208
+ await nomba.bills.lookupCableTvCustomer({
209
+ customerId: "7034512345",
210
+ cableTvType: "dstv",
211
+ });
212
+ await nomba.bills.payCableTv({
213
+ cableTvType: "dstv",
214
+ customerId: "7034512345",
215
+ amount: 15700,
216
+ });
217
+
218
+ await nomba.bills.lookupBettingCustomer({ providerId: "bet9ja", customerId: "12345" });
219
+ await nomba.bills.fundBetting({
220
+ bettingProvider: "bet9ja",
221
+ customerId: "12345",
222
+ amount: 1000,
223
+ });
224
+ ```
225
+
226
+ Also: `listElectricityProviders`, `listCableTvPlans` and `listBettingProviders`. Purchases use the v2 endpoints.
227
+
228
+ ### Global payout
229
+
230
+ ```ts
231
+ const rates = await nomba.globalPayout.getExchangeRates({ from: "NGN", to: "USD" });
232
+ const methods = await nomba.globalPayout.listPaymentMethods({
233
+ destinationCurrency: "CAD",
234
+ });
235
+ const quote = await nomba.globalPayout.convertMoney({
236
+ amount: 100000,
237
+ currency: "NGN",
238
+ destinationCurrency: "CAD",
239
+ transactionType: "SELL",
240
+ });
241
+ const payout = await nomba.globalPayout.authorizeTransfer({
242
+ ...transferDetails,
243
+ lockedExchangeRateId: quote.exchangeRateId,
244
+ idempotencyKey: "payout-77",
245
+ });
246
+ await nomba.globalPayout.getTransaction(payout.wtTransactionId!);
247
+ ```
248
+
249
+ Also: `listAccounts`, `getAccount`, `listBankProviders` and `authorizeExchange`. In the sandbox, `listAccounts` and `getAccount` use Nomba's `/v1/sandbox/global-payout/accounts` paths automatically.
250
+
251
+ ### Global collection
252
+
253
+ ```ts
254
+ const providers = await nomba.globalCollection.listDrcProviders();
255
+ const inflow = await nomba.globalCollection.initiateInflow({
256
+ phoneNumber: "243810000000",
257
+ amount: 10,
258
+ currency: "USD",
259
+ topupVendor: providers[0]!.code!,
260
+ callbackUrl: "https://example.com/inflow",
261
+ idempotencyKey: "inflow-9",
262
+ });
263
+ await nomba.globalCollection.getTransaction(inflow.transactionReference!);
264
+ ```
265
+
266
+ ### Auth
267
+
268
+ ```ts
269
+ await nomba.auth.revoke(); // revokes the current access token and clears the token store
270
+ ```
271
+
272
+ ## Errors
273
+
274
+ Every error extends `NombaError`.
275
+
276
+ | Class | When |
277
+ | -------------------------- | ---------------------------------------------------------------------------------------------------- |
278
+ | `ApiError` | A 2xx response with a non-success `code`, or an HTTP error not listed below. Parent of the next six. |
279
+ | `AuthenticationError` | 401 after the automatic re-authentication, or the token request was rejected. |
280
+ | `PermissionDeniedError` | 403. |
281
+ | `NotFoundError` | 404. |
282
+ | `ValidationError` | 400, 422, or bad input caught before sending (`httpStatus` is `null`). |
283
+ | `RateLimitError` | 429. `retryAfter` (seconds) and `rateLimit` (`X-Rate-Limit-*` headers). |
284
+ | `ServerError` | 5xx. |
285
+ | `ConnectionError` | Network failure. |
286
+ | `TimeoutError` | The request timed out (extends `ConnectionError`). |
287
+ | `WebhookVerificationError` | A webhook failed verification. |
288
+ | `ConfigurationError` | Missing or invalid client options. |
289
+
290
+ `ApiError` has `httpStatus`, `code`, `description` (Nomba's message), `body` (the parsed response), `method` and `path`:
291
+
292
+ ```ts
293
+ import { ApiError, RateLimitError } from "@nombaoss/nomba-node";
294
+
295
+ try {
296
+ await nomba.transfers.toBank(params);
297
+ } catch (err) {
298
+ if (err instanceof RateLimitError) console.log(`retry in ${err.retryAfter}s`);
299
+ else if (err instanceof ApiError) console.log(err.message);
300
+ // Nomba API error 400 (code "400"): Invalid bank code [POST /v2/transfers/bank]
301
+ else throw err;
302
+ }
303
+ ```
304
+
305
+ Errors never contain your client secret, tokens, request headers or query strings.
306
+
307
+ ## Retries and idempotency
308
+
309
+ ### Retries
310
+
311
+ GET requests and token requests are retried automatically on connection errors, timeouts, 429 and 5xx responses, up to `maxRetries` times (default 2). The delay starts at 0.5 s and doubles each time (1 s, 2 s, up to 8 s), with jitter. On a 429 the SDK waits for `Retry-After` (up to 60 s).
312
+
313
+ **POST, PUT and DELETE are never retried automatically.** A timeout or a 5xx on a transfer does not tell you whether Nomba moved the money. Retrying blindly could pay twice. The one exception: after a 401, the SDK gets a new token and retries the request once, because Nomba rejected it before processing it.
314
+
315
+ ### Idempotency with `merchantTxRef`
316
+
317
+ `transfers.toBank`, `transfers.toWallet` and every `bills` purchase send a `merchantTxRef`. If you don't pass one, the SDK generates a random UUID v4 and adds it to the result:
318
+
319
+ ```ts
320
+ const t = await nomba.transfers.toWallet({ amount: 500, receiverAccountId: "..." });
321
+ console.log(t.merchantTxRef); // "3b241101-e2bb-4255-8caf-4136c566a962"
322
+ ```
323
+
324
+ If the call throws, the error carries the reference that was sent, generated or yours, as `err.reference` (`{ field: "merchantTxRef", value }`). After a `TimeoutError`, `ConnectionError` or `ServerError` you don't know whether the money moved. Requery with `err.reference.value` before you retry, and retry only with the same reference:
325
+
326
+ ```ts
327
+ import { NombaError, TransactionStatus } from "@nombaoss/nomba-node";
328
+
329
+ try {
330
+ await nomba.transfers.toBank(details);
331
+ } catch (err) {
332
+ if (!(err instanceof NombaError) || !err.reference) throw err;
333
+ // Outcome unknown: check first.
334
+ const tx = await nomba.transactions.get({ merchantTxRef: err.reference.value });
335
+ if (tx.status !== TransactionStatus.SUCCESS) {
336
+ // Not delivered yet: wait for the webhook, or retry with the SAME reference.
337
+ // nomba.transfers.toBank({ ...details, merchantTxRef: err.reference.value })
338
+ }
339
+ }
340
+ ```
341
+
342
+ (`transactions.get` throws `NotFoundError` if Nomba never received the transfer; then retrying with the same reference is safe.)
343
+
344
+ Better still: pass your own reference and store it before you send, so you can check the outcome even if your process crashes mid-request. Nomba deduplicates on it:
345
+
346
+ ```ts
347
+ const merchantTxRef = `payout-${invoice.id}`;
348
+ await nomba.transfers.toBank({ ...details, merchantTxRef });
349
+ ```
350
+
351
+ `checkout.chargeSavedCard` has no `merchantTxRef` field; its reference is `order.orderReference`. The SDK generates that one the same way when you omit it, returns it as `orderReference`, and sets `err.reference` to `{ field: "orderReference", value }` on errors. Global payout and collection calls take an `idempotencyKey`, which you set yourself.
352
+
353
+ ## Transaction statuses
354
+
355
+ The SDK returns `data.status` as-is and never throws because of it. Check it yourself. The values are exported as `TransactionStatus`:
356
+
357
+ | `TransactionStatus` | What happened | Refund your customer? |
358
+ | -------------------- | ------------------------------------ | --------------------- |
359
+ | `SUCCESS` | Service delivered to the destination | No |
360
+ | `REFUND` | Funds returned to your Nomba wallet | Yes |
361
+ | `CANCELLED` | The transaction will not proceed | Contact support |
362
+ | `PAYMENT_FAILED` | Debit failed; Nomba may retry | Wait for the webhook |
363
+ | `REVERSED_BY_VENDOR` | Nomba may retry via another vendor | Wait for the webhook |
364
+ | `PENDING_BILLING` | Still processing | Wait for the webhook |
365
+ | `PROCESSING` | Still processing (v1 endpoints) | Wait for the webhook |
366
+
367
+ If you get a status that isn't listed, requery or wait for a webhook. Only refund on a `payout_refund` webhook or a requery that returns `REFUND`. Source: [Response codes](https://developer.nomba.com/docs/api-basics/response-codes).
368
+
369
+ ## Pagination
370
+
371
+ List endpoints return one page: `{ results, cursor }`. An empty or missing `cursor` means it's the last page. `limit` can be 1 to 50.
372
+
373
+ ```ts
374
+ const page = await nomba.transactions.list({ limit: 50 });
375
+ const next = await nomba.transactions.list({ limit: 50, cursor: page.cursor });
376
+ ```
377
+
378
+ The auto-pagers fetch pages as you iterate:
379
+
380
+ ```ts
381
+ for await (const tx of nomba.transactions.listAll({ dateFrom: "2026-01-01T00:00:00" })) {
382
+ console.log(tx.id, tx.amount);
383
+ }
384
+ ```
385
+
386
+ | Page method | Auto-pager |
387
+ | ----------------------------------- | -------------------------------------- |
388
+ | `transactions.list` | `transactions.listAll` |
389
+ | `transactions.filter` | `transactions.filterAll` |
390
+ | `transactions.listBankTransactions` | `transactions.listAllBankTransactions` |
391
+ | `accounts.listTerminals` | `accounts.listAllTerminals` |
392
+ | `virtualAccounts.list` | `virtualAccounts.listAll` |
393
+
394
+ `checkout.listSavedCards` and `directDebits.listMandates` use page numbers (`page`, `nextPage` / `hasMore`) instead.
395
+
396
+ ## Webhooks
397
+
398
+ `verifyWebhook(rawBody, headers, signingKey)` checks the `nomba-signature` header and returns the parsed event, or throws `WebhookVerificationError`. Webhook handlers don't need API credentials, so it works without a client. It is also available as `Nomba.webhooks.verify` and `nomba.webhooks.verify`.
399
+
400
+ ```ts
401
+ import express from "express";
402
+ import { verifyWebhook, WebhookEventType } from "@nombaoss/nomba-node";
403
+
404
+ const app = express();
405
+
406
+ app.post("/webhooks/nomba", express.raw({ type: "*/*" }), (req, res) => {
407
+ let event;
408
+ try {
409
+ event = verifyWebhook(req.body, req.headers, process.env.NOMBA_WEBHOOK_SIGNING_KEY!, {
410
+ toleranceSeconds: 300,
411
+ });
412
+ } catch {
413
+ return res.sendStatus(401);
414
+ }
415
+ if (event.event_type === WebhookEventType.PAYMENT_SUCCESS) {
416
+ // fulfil the order; deduplicate on event.requestId
417
+ }
418
+ res.sendStatus(200);
419
+ });
420
+ ```
421
+
422
+ **Pass the raw body.** Use `express.raw()` (or `await request.text()` with the Fetch API), not `express.json()`. If the body is parsed and serialised again, it can change and the signature will not match. `rawBody` can be a string, `Buffer`, `Uint8Array` or `ArrayBuffer`. `headers` can be Node's `req.headers` or a Fetch `Headers` object.
423
+
424
+ - The signature is compared exactly (base64 is case-sensitive) and in constant time. Surrounding whitespace in the header is ignored.
425
+ - `nomba-sig-value` is accepted if `nomba-signature` is absent. A `nomba-signature-algorithm` other than `HmacSHA256` is rejected.
426
+ - `nomba-timestamp` must be an RFC 3339 time (for example `2025-09-29T10:51:44Z`), and a `responseCode` containing `:` is rejected. Both checks stop a forged webhook from moving part of the timestamp into the signed body.
427
+ - `toleranceSeconds` (off by default) rejects events whose `nomba-timestamp` is more than that many seconds from now, which protects against replayed requests.
428
+
429
+ Event types are exported as `WebhookEventType`: `payment_success`, `payment_failed`, `payment_reversal`, `payout_success`, `payout_failed`, `payout_refund`. Verification uses `node:crypto`, so it works in Node, Bun, Deno and edge runtimes with Node compatibility enabled.
430
+
431
+ ## Token store
432
+
433
+ Nomba access tokens last about 30 minutes. The SDK refreshes them 5 minutes before they expire, and concurrent requests share a single token request. By default each client keeps its token in memory.
434
+
435
+ If you run several instances, Nomba recommends that one token is shared rather than each instance authenticating. Pass a `tokenStore` with `get` and `set` (both may be async):
436
+
437
+ ```ts
438
+ import Nomba, { type Token, type TokenStore } from "@nombaoss/nomba-node";
439
+
440
+ const redisStore: TokenStore = {
441
+ async get() {
442
+ const v = await redis.get("nomba:token");
443
+ return v ? (JSON.parse(v) as Token) : null;
444
+ },
445
+ async set(token) {
446
+ if (token) await redis.set("nomba:token", JSON.stringify(token));
447
+ else await redis.del("nomba:token");
448
+ },
449
+ };
450
+
451
+ const nomba = new Nomba({ tokenStore: redisStore });
452
+ ```
453
+
454
+ A `Token` is `{ accessToken, refreshToken, expiresAt }`. Treat the store as a secret.
455
+
456
+ ## Sandbox and production
457
+
458
+ The client uses the sandbox unless you set `environment: "production"` (or `NOMBA_ENVIRONMENT=production`), so you can't reach production by accident. Sandbox and production use different credentials. Any value other than `sandbox` or `production` throws a `ConfigurationError`.
459
+
460
+ ## Links
461
+
462
+ - [Nomba API documentation](https://developer.nomba.com)
463
+ - [OpenAPI spec (NombaOSS/openapi)](https://github.com/NombaOSS/openapi)
464
+ - [Changelog](CHANGELOG.md), [Security policy](SECURITY.md)
465
+
466
+ ## License
467
+
468
+ MIT