@nile-squad/nylonpay-ts 1.0.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.
@@ -0,0 +1,573 @@
1
+ import { Result } from 'slang-ts';
2
+
3
+ /**
4
+ * Lifecycle states a transaction can occupy. Merchants use these to drive
5
+ * fulfillment logic: trigger order completion on "successful", notify
6
+ * customer on "failed", release inventory on "cancelled".
7
+ */
8
+ type TransactionStatus = "pending" | "processing" | "successful" | "failed" | "cancelled";
9
+ /**
10
+ * Broad transaction categories. Distinguishes collections from payouts
11
+ * so merchants can route webhooks and reports to the right subsystems.
12
+ */
13
+ type TransactionType = "collection" | "payout" | "transfer" | "escrow" | "refund" | "reversal" | "charge" | "chargeback";
14
+ /**
15
+ * Supported payment rails. Mobile money is the default for most East African
16
+ * integrations; bank transfers are used for larger-ticket collections or
17
+ * payouts to corporate accounts.
18
+ */
19
+ type PaymentMethod = "mobileMoney" | "bank";
20
+ /**
21
+ * Transaction execution mode. Test transactions are routed through sandbox
22
+ * providers and do not deduct real funds. Live transactions use production
23
+ * provider credentials.
24
+ */
25
+ type TransactionMode = "test" | "live";
26
+ /**
27
+ * Events emitted by a PaymentInstance as a transaction progresses.
28
+ * Merchants subscribe to these to react to status changes without
29
+ * manually polling.
30
+ */
31
+ type PaymentEvent = "processing" | "success" | "failed" | "cancelled" | "error";
32
+ /**
33
+ * Webhook event types delivered to the merchant's configured endpoint.
34
+ * Merchants use these to update internal order state, send customer
35
+ * notifications, and reconcile ledgers without polling.
36
+ */
37
+ type WebhookEventType = "collection.completed" | "collection.failed" | "payout.completed" | "payout.failed" | "payout.reversed" | "refund.completed" | "chargeback.received";
38
+ /**
39
+ * Currencies supported by the platform. Merchants should use the currency
40
+ * matching their settlement account to avoid FX surprises.
41
+ */
42
+ type Currency = "USD" | "EUR" | "GBP" | "KES" | "UGX" | "TZS" | "RWF";
43
+ /**
44
+ * Customer details attached to a payment. The phone number is the primary
45
+ * identity for mobile-money collections; email is optional and used for
46
+ * receipts when available.
47
+ */
48
+ type Customer = {
49
+ name: string;
50
+ phoneNumber: string;
51
+ email?: string;
52
+ };
53
+ /**
54
+ * Destination account for a payout. The account holder name must match
55
+ * KYC records to reduce reversal risk.
56
+ */
57
+ type Destination = {
58
+ accountHolderName: string;
59
+ accountNumber: string;
60
+ bankName?: string;
61
+ phone?: string;
62
+ };
63
+ /**
64
+ * Line item for an invoice. Merchants use these to render itemized
65
+ * breakdowns on the hosted payment page.
66
+ */
67
+ type InvoiceItem = {
68
+ name: string;
69
+ quantity: number;
70
+ unitPrice: number;
71
+ };
72
+ /**
73
+ * Bank account details required when the payment method is "bank".
74
+ */
75
+ type BankDetails = {
76
+ accountNumber: string;
77
+ bankName: string;
78
+ };
79
+ /**
80
+ * Input for initiating a collection. The SDK abstracts provider routing,
81
+ * so the merchant only specifies what to collect, from whom, and how.
82
+ */
83
+ type CollectPaymentInput = {
84
+ amount: number;
85
+ currency: Currency;
86
+ customer: Customer;
87
+ description: string;
88
+ reference?: string;
89
+ method?: PaymentMethod;
90
+ bank?: BankDetails;
91
+ metadata?: Record<string, string>;
92
+ };
93
+ /**
94
+ * Input for initiating a payout. Use this to disburse funds to a
95
+ * customer's bank account or mobile-money wallet.
96
+ */
97
+ type MakePayoutInput = {
98
+ amount: number;
99
+ currency: Currency;
100
+ customer: Customer;
101
+ destination: Destination;
102
+ description: string;
103
+ reference?: string;
104
+ metadata?: Record<string, string>;
105
+ };
106
+ /**
107
+ * Input for a one-shot status check. Does not start polling; returns
108
+ * the current server-side state of the transaction.
109
+ */
110
+ type GetStatusInput = {
111
+ reference: string;
112
+ };
113
+ /**
114
+ * Input for looking up a full transaction record. At least one of
115
+ * `id` or `reference` must be provided.
116
+ */
117
+ type GetTransactionInput = {
118
+ id?: string;
119
+ reference?: string;
120
+ };
121
+ /**
122
+ * Input for phone-number pre-validation. Returns the registered name
123
+ * on the account so merchants can confirm customer identity before
124
+ * initiating a collection or payout.
125
+ */
126
+ type VerifyPhoneInput = {
127
+ phoneNumber: string;
128
+ purpose?: "collection" | "payout";
129
+ };
130
+ /**
131
+ * Input for creating a hosted invoice. The returned URL can be shared
132
+ * with customers; card payments are only supported via this hosted
133
+ * flow to keep the merchant out of PCI scope.
134
+ */
135
+ type CreateInvoiceInput = {
136
+ amount: number;
137
+ currency: Currency;
138
+ description: string;
139
+ items?: InvoiceItem[];
140
+ redirectUrl?: string;
141
+ reference?: string;
142
+ metadata?: Record<string, string>;
143
+ };
144
+ /**
145
+ * Input for verifying a webhook signature. Operates on raw payload bytes
146
+ * to avoid re-serialization altering the signed content.
147
+ */
148
+ type VerifyWebhookInput = {
149
+ payload: string | Uint8Array;
150
+ signature: string;
151
+ secret: string;
152
+ };
153
+ /**
154
+ * Full transaction record returned by lookups, event handlers, and the
155
+ * blocking resolve variants. Contains everything a merchant needs to
156
+ * reconcile without leaking internal provider details.
157
+ */
158
+ type Transaction = {
159
+ id: string;
160
+ reference: string;
161
+ amount: number;
162
+ currency: Currency;
163
+ status: TransactionStatus;
164
+ type: TransactionType;
165
+ method: PaymentMethod;
166
+ description: string;
167
+ phone: string;
168
+ email: string | null;
169
+ failureReason: string | null;
170
+ metadata: Record<string, string>;
171
+ mode: TransactionMode;
172
+ createdAt: string;
173
+ updatedAt: string;
174
+ };
175
+ /**
176
+ * Lightweight status response for quick checks. Use when you only need
177
+ * the current state, not the full transaction record.
178
+ */
179
+ type StatusResponse = {
180
+ reference: string;
181
+ status: TransactionStatus;
182
+ amount: number;
183
+ currency: Currency;
184
+ updatedAt: string;
185
+ };
186
+ /**
187
+ * Result of a phone verification call. `verified` is true when the
188
+ * provider confirms the number is active and the returned name matches
189
+ * expectations.
190
+ */
191
+ type PhoneVerification = {
192
+ phoneNumber: string;
193
+ customerName: string;
194
+ verified: boolean;
195
+ };
196
+ /**
197
+ * Response from creating an invoice. The `url` is customer-facing;
198
+ * the `token` can be used to idempotently re-fetch the invoice state.
199
+ */
200
+ type InvoiceResponse = {
201
+ id: string;
202
+ url: string;
203
+ token: string;
204
+ expiresAt: string;
205
+ status: "pending";
206
+ };
207
+ /**
208
+ * Structured payload delivered to the merchant's webhook endpoint.
209
+ * Merchants should verify the signature before trusting the data.
210
+ */
211
+ type WebhookPayload = {
212
+ event: WebhookEventType;
213
+ data: Transaction;
214
+ timestamp: string;
215
+ signature: string;
216
+ };
217
+ /**
218
+ * SDK configuration supplied by the merchant at initialization.
219
+ * All timeouts and retry limits are configurable for different
220
+ * network environments.
221
+ *
222
+ * Test vs. live mode is determined by the API key, not by config — a sandbox
223
+ * key routes to test providers, a live key processes real money.
224
+ */
225
+ type NylonPayConfig = {
226
+ apiKey: string;
227
+ apiSecret: string;
228
+ baseUrl?: string;
229
+ timeoutMs?: number;
230
+ maxRetries?: number;
231
+ maxPollIntervalMs?: number;
232
+ maxPollDurationMs?: number;
233
+ maxPollAttempts?: number;
234
+ fetch?: typeof globalThis.fetch;
235
+ };
236
+ /**
237
+ * Structured error returned by SDK operations. `code` is machine-readable
238
+ * for branching logic; `message` is human-readable for logs and alerts.
239
+ * `retryable` tells the merchant whether the same request may succeed
240
+ * on re-invocation.
241
+ * @internal
242
+ */
243
+ type SdkError = {
244
+ code: string;
245
+ message: string;
246
+ statusCode?: number;
247
+ retryable?: boolean;
248
+ };
249
+ /**
250
+ * Data passed to every payment event handler. `transaction` is populated
251
+ * for status-change events (`processing`, `success`, `failed`, `cancelled`);
252
+ * `error` is populated for the `"error"` event (network failure, timeout,
253
+ * reference mismatch).
254
+ *
255
+ * @example
256
+ * ```ts
257
+ * payment.on("success", (data: EventData) => {
258
+ * console.log(data.transaction?.reference); // "ORDER-123"
259
+ * console.log(data.timestamp); // "2026-05-30T12:00:00.000Z"
260
+ * });
261
+ * ```
262
+ */
263
+ type EventData = {
264
+ /** The event that triggered this handler. */
265
+ event: PaymentEvent;
266
+ /** Full transaction record. Present for status-change events. */
267
+ transaction?: Transaction;
268
+ /** Error message. Present for the `"error"` event. */
269
+ error?: string;
270
+ /** ISO 8601 timestamp of when the event was emitted. */
271
+ timestamp: string;
272
+ };
273
+ /**
274
+ * Callback signature for {@link PaymentInstance} event handlers.
275
+ * Receives an {@link EventData} object with the event type, transaction
276
+ * data (if available), and timestamp.
277
+ */
278
+ type PaymentEventHandler = (data: EventData) => void;
279
+ /**
280
+ * SDK instance returned by the factory. Provides all payment operations
281
+ * and the webhook verification utility.
282
+ *
283
+ * Using an interface here because it defines a contract of methods
284
+ * that an object must satisfy, which is the idiomatic use of interface
285
+ * per project conventions.
286
+ */
287
+ interface NylonPaySdk {
288
+ /**
289
+ * Initiate a payment collection from a customer's phone or bank account.
290
+ * Returns a {@link PaymentInstance} that polls for status updates and emits
291
+ * events (`processing`, `success`, `failed`, `cancelled`, `error`).
292
+ *
293
+ * Auto-generates an idempotency `reference` if omitted. Throws on invalid
294
+ * input (zero amount, empty phone, bank method without bank details).
295
+ *
296
+ * @example
297
+ * ```ts
298
+ * const payment = await nylonpay.collectPayment({
299
+ * amount: 10000,
300
+ * currency: "UGX",
301
+ * customer: { name: "Jane", phoneNumber: "+256700000000" },
302
+ * description: "Order #1234",
303
+ * });
304
+ *
305
+ * payment.on("success", ({ transaction }) => fulfillOrder(transaction));
306
+ * payment.on("failed", ({ error }) => notifyCustomer(error));
307
+ * ```
308
+ */
309
+ collectPayment(input: CollectPaymentInput): Promise<PaymentInstance>;
310
+ /**
311
+ * Initiate a collection and block until the transaction reaches a terminal
312
+ * state. The server polls internally — this is a single request/response
313
+ * call, not client-side polling. Use for CLI tools, serverless functions,
314
+ * or simple scripts that don't need event-driven updates.
315
+ *
316
+ * @example
317
+ * ```ts
318
+ * const result = await nylonpay.collectPaymentAndResolve({
319
+ * amount: 5000,
320
+ * currency: "UGX",
321
+ * customer: { name: "Jane", phoneNumber: "+256700000000" },
322
+ * description: "Quick payment",
323
+ * });
324
+ *
325
+ * if (result.isOk) console.log("Paid:", result.value.reference);
326
+ * ```
327
+ */
328
+ collectPaymentAndResolve(input: CollectPaymentInput): Promise<Result<Transaction, string>>;
329
+ /**
330
+ * Initiate a disbursement to a destination account (bank or mobile money).
331
+ * Returns a {@link PaymentInstance} that polls for status updates and emits
332
+ * events as the payout progresses.
333
+ *
334
+ * Auto-generates an idempotency `reference` if omitted.
335
+ *
336
+ * @example
337
+ * ```ts
338
+ * const payout = await nylonpay.makePayout({
339
+ * amount: 50000,
340
+ * currency: "UGX",
341
+ * customer: { name: "Jane", phoneNumber: "+256700000000" },
342
+ * destination: { accountHolderName: "Jane Doe", accountNumber: "123456" },
343
+ * description: "Refund for order #1234",
344
+ * });
345
+ *
346
+ * const tx = await payout.wait();
347
+ * ```
348
+ */
349
+ makePayout(input: MakePayoutInput): Promise<PaymentInstance>;
350
+ /**
351
+ * Initiate a disbursement and block until the payout reaches a terminal
352
+ * state. The server polls internally — single request/response call.
353
+ *
354
+ * @example
355
+ * ```ts
356
+ * const result = await nylonpay.makePayoutAndResolve({
357
+ * amount: 50000,
358
+ * currency: "UGX",
359
+ * customer: { name: "Jane", phoneNumber: "+256700000000" },
360
+ * destination: { accountHolderName: "Jane Doe", accountNumber: "123456" },
361
+ * description: "Refund",
362
+ * });
363
+ * ```
364
+ */
365
+ makePayoutAndResolve(input: MakePayoutInput): Promise<Result<Transaction, string>>;
366
+ /**
367
+ * One-shot status check for a transaction. Does not poll — returns the
368
+ * current server-side state. Use for lightweight checks or when you
369
+ * already have the reference from a webhook or previous call.
370
+ *
371
+ * @example
372
+ * ```ts
373
+ * const result = await nylonpay.getStatus({ reference: "ORDER-123" });
374
+ * if (result.isOk) console.log(result.value.status); // "successful"
375
+ * ```
376
+ */
377
+ getStatus(input: GetStatusInput): Promise<Result<StatusResponse, string>>;
378
+ /**
379
+ * Look up a full transaction record by `id` or `reference`. At least one
380
+ * must be provided. Returns the complete transaction including failure
381
+ * reason, metadata, and timestamps.
382
+ *
383
+ * @example
384
+ * ```ts
385
+ * const result = await nylonpay.getTransaction({ reference: "ORDER-123" });
386
+ * if (result.isOk) console.log(result.value.failureReason);
387
+ * ```
388
+ */
389
+ getTransaction(input: GetTransactionInput): Promise<Result<Transaction, string>>;
390
+ /**
391
+ * Pre-validate a phone number with the payment provider. Returns the
392
+ * registered name on the account so you can confirm customer identity
393
+ * before initiating a collection or payout.
394
+ *
395
+ * @example
396
+ * ```ts
397
+ * const result = await nylonpay.verifyPhone({ phoneNumber: "+256700000000" });
398
+ * if (result.isOk && result.value.verified) {
399
+ * console.log("Registered to:", result.value.customerName);
400
+ * }
401
+ * ```
402
+ */
403
+ verifyPhone(input: VerifyPhoneInput): Promise<Result<PhoneVerification, string>>;
404
+ /**
405
+ * Generate a hosted payment link. The returned URL renders a payment page
406
+ * where the customer completes the transaction — including card payments
407
+ * (the only way to accept cards, keeping you out of PCI scope).
408
+ *
409
+ * Supports optional line items (max 50) for itemized breakdowns on the
410
+ * payment page. Auto-generates `reference` if omitted.
411
+ *
412
+ * @example
413
+ * ```ts
414
+ * const result = await nylonpay.createInvoice({
415
+ * amount: 25000,
416
+ * currency: "UGX",
417
+ * description: "Monthly subscription",
418
+ * items: [{ name: "Pro Plan", quantity: 1, unitPrice: 25000 }],
419
+ * redirectUrl: "https://myapp.com/thank-you",
420
+ * });
421
+ *
422
+ * if (result.isOk) sendEmail(result.value.url);
423
+ * ```
424
+ */
425
+ createInvoice(input: CreateInvoiceInput): Promise<Result<InvoiceResponse, string>>;
426
+ /**
427
+ * Verify that an incoming webhook payload was signed by Nylon Pay.
428
+ * Operates on raw payload bytes (string or Uint8Array), not parsed JSON,
429
+ * to prevent re-serialization from altering the signed content.
430
+ *
431
+ * Call this in your webhook handler before trusting the event data.
432
+ *
433
+ * @example
434
+ * ```ts
435
+ * app.post("/webhooks", (req, res) => {
436
+ * const isValid = nylonpay.verifyWebhookSignature({
437
+ * payload: req.rawBody,
438
+ * signature: req.headers["x-nylon-signature"],
439
+ * secret: process.env.NYLONPAY_WEBHOOK_SECRET,
440
+ * });
441
+ *
442
+ * if (!isValid) return res.status(401).send("Invalid signature");
443
+ * // Process the verified webhook event
444
+ * });
445
+ * ```
446
+ */
447
+ verifyWebhookSignature(input: VerifyWebhookInput): boolean;
448
+ }
449
+ /**
450
+ * Event-driven handle for an async payment operation. Subscribe to status
451
+ * transitions with `on`/`once`/`off`, or block until completion with `wait`.
452
+ *
453
+ * Using an interface here because it defines a contract of methods
454
+ * that an object must satisfy, which is the idiomatic use of interface
455
+ * per project conventions.
456
+ */
457
+ interface PaymentInstance {
458
+ /** The transaction reference. Immutable after creation. */
459
+ readonly reference: string;
460
+ /** Current transaction status. `null` until the first poll resolves. */
461
+ readonly status: TransactionStatus | null;
462
+ /**
463
+ * Register a handler for a payment event. Fires every time the event
464
+ * occurs. Returns the instance for chaining.
465
+ *
466
+ * Events: `"processing"`, `"success"`, `"failed"`, `"cancelled"`, `"error"`
467
+ *
468
+ * @example
469
+ * ```ts
470
+ * payment.on("success", ({ transaction }) => fulfillOrder(transaction));
471
+ * payment.on("error", ({ error }) => log.error(error));
472
+ * ```
473
+ */
474
+ on(event: PaymentEvent, handler: PaymentEventHandler): PaymentInstance;
475
+ /**
476
+ * Register a handler that fires at most once, then auto-unsubscribes.
477
+ * Useful for one-shot terminal event handlers. Returns the instance.
478
+ *
479
+ * @example
480
+ * ```ts
481
+ * payment.once("success", ({ transaction }) => sendReceipt(transaction));
482
+ * ```
483
+ */
484
+ once(event: PaymentEvent, handler: PaymentEventHandler): PaymentInstance;
485
+ /**
486
+ * Remove a previously registered handler. Safe to call for a handler
487
+ * that was never registered. Returns the instance.
488
+ */
489
+ off(event: PaymentEvent, handler: PaymentEventHandler): PaymentInstance;
490
+ /**
491
+ * Block until the transaction reaches a terminal state. Resolves with
492
+ * the full {@link Transaction} on success. Rejects on failure,
493
+ * cancellation, or polling error.
494
+ *
495
+ * @example
496
+ * ```ts
497
+ * try {
498
+ * const tx = await payment.wait();
499
+ * console.log("Paid:", tx.amount, tx.currency);
500
+ * } catch (err) {
501
+ * console.error("Payment did not succeed:", err.message);
502
+ * }
503
+ * ```
504
+ */
505
+ wait(): Promise<Transaction>;
506
+ }
507
+
508
+ /**
509
+ * Factory function to create a Nylon Pay SDK instance.
510
+ * This is the main entry point for merchants.
511
+ *
512
+ * @example
513
+ * ```ts
514
+ * import { createNylonPay } from "@nile-squad/nylonpay-ts";
515
+ *
516
+ * export const nylonpay = createNylonPay({
517
+ * apiKey: process.env.NYLONPAY_API_KEY!,
518
+ * apiSecret: process.env.NYLONPAY_API_SECRET!,
519
+ * });
520
+ * ```
521
+ */
522
+
523
+ /**
524
+ * Create a Nylon Pay SDK instance.
525
+ *
526
+ * @param config - SDK configuration with apiKey and apiSecret
527
+ * @returns SDK instance with all payment operations
528
+ *
529
+ * @throws Error if required config is missing or invalid
530
+ */
531
+ declare function createNylonPay(config: NylonPayConfig): NylonPaySdk;
532
+
533
+ /**
534
+ * HTTP transport layer for SDK communication with the Nylon Pay backend.
535
+ * Handles the Nile.js envelope format, HMAC request signing, response
536
+ * signature verification, retries, and timeouts.
537
+ *
538
+ * @internal
539
+ */
540
+
541
+ /**
542
+ * Parse an error string into an SdkError object.
543
+ * Tries JSON.parse first; falls back to a generic UNKNOWN error.
544
+ *
545
+ * @example
546
+ * ```ts
547
+ * const result = await sdk.getStatus({ reference: "ORDER-123" });
548
+ * if (!result.isOk) {
549
+ * const error = parseError(result.error);
550
+ * console.log(error.code, error.message);
551
+ * }
552
+ * ```
553
+ */
554
+ declare function parseError(error: string): SdkError;
555
+
556
+ /**
557
+ * Standalone webhook signature verification utility.
558
+ * Merchants use this to confirm that incoming webhook payloads
559
+ * were genuinely sent by Nylon Pay before acting on them.
560
+ */
561
+
562
+ /**
563
+ * Verify that a webhook payload was signed by Nylon Pay.
564
+ * Operates on raw payload bytes, NOT parsed JSON (spec invariant #8).
565
+ *
566
+ * @param input.payload - Raw request body as string or Uint8Array
567
+ * @param input.signature - Signature from the webhook header
568
+ * @param input.secret - Merchant's webhook secret
569
+ * @returns True when the signature is valid
570
+ */
571
+ declare function verifyWebhookSignature(input: VerifyWebhookInput): boolean;
572
+
573
+ export { type BankDetails, type CollectPaymentInput, type CreateInvoiceInput, type Currency, type Customer, type Destination, type EventData, type GetStatusInput, type GetTransactionInput, type InvoiceItem, type InvoiceResponse, type MakePayoutInput, type NylonPayConfig, type NylonPaySdk, type PaymentEvent, type PaymentEventHandler, type PaymentInstance, type PaymentMethod, type PhoneVerification, type SdkError, type StatusResponse, type Transaction, type TransactionMode, type TransactionStatus, type TransactionType, type VerifyPhoneInput, type VerifyWebhookInput, type WebhookEventType, type WebhookPayload, createNylonPay, parseError, verifyWebhookSignature };