Server-side SDK for integrating Nylon Pay into merchant applications. Supports TypeScript and JavaScript (ESM and CJS).
This package is the reference implementation of the Nylon Pay SDK Spec, the canonical, language-agnostic contract for building Nylon Pay SDKs in any language.
npm install @nile-squad/nylonpay-tsimport { createNylonPay } from "@nile-squad/nylonpay-ts";
const nylonpay = createNylonPay({
apiKey: "npk_test_...",
apiSecret: "nps_test_...",
onError: (error) => {
if (error.reason === "SERVICES_DOWN") {
pausePaymentAttempts(error.message);
}
},
});
const payment = await nylonpay.collectPayment({
amount: 10000,
currency: "UGX",
customer: { name: "Jane", phoneNumber: "+256700000000" },
description: "Order #1234",
});
payment.on("success", ({ transaction }) => fulfillOrder(transaction));
payment.on("failed", ({ error }) => notifyCustomer(error));Use your test keys to work in sandbox, or your production keys to go live.
| Option | Required | Default | Description |
|---|---|---|---|
apiKey |
Yes | Must start with npk_ |
|
apiSecret |
Yes | Must start with nps_ |
|
baseUrl |
No | https://api.nylonpay.com/api/services |
Override for a custom endpoint. LEGACY_BASE_URL is the original address, still served |
timeoutMs |
No | 90000 |
Request timeout in milliseconds |
maxRetries |
No | 3 |
Retry count for failed requests |
maxPollIntervalMs |
No | 2000 |
Polling interval for async payments |
maxPollDurationMs |
No | (none) | Optional cap on total polling time. Omit to wait until terminal. |
maxPollAttempts |
No | (none) | Optional cap on poll count. Omit to wait until terminal. |
onDelayed |
No | "wait" |
"return" hands back a delayed still-pending payment; "wait" keeps polling |
onError |
No | (none) | Global handler for structured operation errors on this SDK instance |
Initiate a payment collection. Returns a PaymentInstance with event-driven updates.
const payment = await nylonpay.collectPayment({
amount: 10000,
currency: "UGX",
customer: { name: "Jane", phoneNumber: "+256700000000" },
description: "Order #1234",
method: "mobileMoney",
reference: "550e8400-e29b-41d4-a716-446655440000",
});
payment.on("success", ({ transaction }) => { /* ... */ });
payment.on("failed", ({ error }) => { /* ... */ });reference is optional and auto-generated if omitted. A supplied reference must be
a valid UUID (any version); the SDK throws a validation error otherwise. Omit the
field to auto-generate a UUID v4.
Block until the collection reaches a terminal state. Single request and response; the server resolves the payment and returns the final state.
const result = await nylonpay.collectPaymentAndResolve({
amount: 5000,
currency: "UGX",
customer: { name: "Jane", phoneNumber: "+256700000000" },
description: "Quick payment",
});
if (result.isOk) console.log("Paid:", result.value.reference);Send funds to a destination mobile money wallet or bank account. destination.phone is required on every payout.
const payout = await nylonpay.makePayout({
amount: 50000,
currency: "UGX",
customer: { name: "Jane", phoneNumber: "+256700000000" },
destination: {
accountHolderName: "Jane Doe",
accountNumber: "123456",
phone: "+256700000000",
},
description: "Refund for order #1234",
});
const tx = await payout.wait();Block until the payout reaches a terminal state. Single request and response.
const result = await nylonpay.makePayoutAndResolve({
amount: 50000,
currency: "UGX",
customer: { name: "Jane", phoneNumber: "+256700000000" },
destination: {
accountHolderName: "Jane Doe",
accountNumber: "123456",
phone: "+256700000000",
},
description: "Refund",
});With a sandbox key, collectPayment, collectPaymentAndResolve, makePayout and makePayoutAndResolve accept two test-only options:
testOutcome: "success" | "fail"(or a failure code) forces the result.prompt: "pin"holds the payment until the merchant types the customer's 5-digit PIN, shown on the dashboard phone simulator. The wait is up to about 2 minutes with 3 tries. The PIN is one per merchant and works with every sandbox key of that merchant.
If both are set, testOutcome wins. With neither, the sandbox decides on its own. A live key rejects both. The AndResolve variants can wait about 2 minutes with prompt, so raise maxPollDurationMs above that if you cap polling.
makePayout returns immediately with a reference for tracking and idempotent retries. The payout status flows through several stages:
pending, Payout accepted and queued for processingprocessing, Payout is being processedon_hold, Payout waiting on review. Polling continues and it finishes on its own assuccessful,failed, orcancelled. From aKES,TZS,RWF, orCDFwallet it also waits for you to confirm the exchange rate on the dashboard; with no answer within 24 hours it endscancelledand the money returns to your wallet.successful, Payout completed; funds sent to destinationfailed, Payout failed; funds refunded to merchant accountcancelled, Payout was cancelled by the merchant
Polling and webhooks: Monitor payout progress by:
- Subscribing to
"processing"events (coverspending,processing, andon_holdstates) - Listening for terminal events:
"success","failed","cancelled" - Receiving webhook notifications at your configured endpoint
The SDK keeps polling while a payout is on_hold and stops when it reaches a final state. statusText carries the review sentence, for example "This payout is being reviewed and will complete shortly."
const payout = await nylonpay.makePayout({ /* ... */ });
payout.on("processing", ({ reference, transaction }) => {
// Payout is in flight (pending, processing, or on_hold)
if (transaction?.status === "on_hold") {
console.log("Payout under review:", transaction.statusText);
// "This payout is being reviewed and will complete shortly"
}
});
payout.on("success", ({ transaction }) => {
console.log("Payout complete:", transaction?.reference);
});
// Or wait for terminal state
const tx = await payout.wait();
if (tx) {
console.log("Final status:", tx.status);
}One-shot status check for a transaction. Returns the current server-side state in a single call.
const result = await nylonpay.getStatus({ reference: "550e8400-e29b-41d4-a716-446655440000" });
if (result.isOk) console.log(result.value.status);Look up a full transaction record by id or reference. At least one must be provided.
const result = await nylonpay.getTransaction({ reference: "550e8400-e29b-41d4-a716-446655440000" });
if (result.isOk) console.log(result.value.failureReason);Pre-validate a phone number and get the registered name.
const result = await nylonpay.verifyPhone({ phoneNumber: "+256700000000" });
if (result.isOk && result.value.verified) {
console.log("Registered to:", result.value.customerName);
}Kenya, Tanzania, Rwanda, and DR Congo numbers work too: pass them with the country
code (+254710000000). A local number starting with 0 is treated as Uganda.
Generate a hosted payment link. Card payments run through this hosted flow. Invoices require an approved Level 2 account in every currency.
const result = await nylonpay.createInvoice({
amount: 25000,
currency: "UGX",
customerEmail: "customer@example.com",
description: "Monthly subscription",
items: [{ name: "Pro Plan", quantity: 1, unitPrice: 25000 }],
});
if (result.isOk) sendEmail(result.value.paymentLink);Verify incoming webhook payloads before processing.
app.post("/webhooks", (req, res) => {
const isValid = nylonpay.verifyWebhookSignature({
payload: req.rawBody,
signature: req.headers["x-nylon-signature"],
secret: "nps_...",
});
if (!isValid) return res.status(401).send("Invalid signature");
});collectPayment and makePayout return a PaymentInstance with event-driven updates. The SDK automatically polls for status changes and emits events as the transaction progresses.
| Event | Description |
|---|---|
processing |
Transaction is being processed (covers pending, processing, and on_hold states) |
success |
Transaction completed successfully |
failed |
Transaction failed |
cancelled |
Transaction was cancelled |
error |
Network or polling error |
A payout on on_hold is waiting on review and completes on its own. From a KES, TZS, RWF, or CDF wallet it also waits for you to confirm the exchange rate on the dashboard; with no answer within 24 hours it ends cancelled and the money returns to your wallet. Polling continues automatically; read transaction?.statusText for the review sentence.
payment.on("success", ({ transaction }) => { /* ... */ });
payment.once("success", ({ transaction }) => { /* fires once */ });
payment.off("success", handler);
const tx = await payment.wait();Delayed payments (v1.5+): By default wait() keeps polling until the payment finishes. After about three minutes in flight, status responses include delayed: true. To hand control back early and rely on webhooks instead:
const nylonpay = createNylonPay({
apiKey: "npk_...",
apiSecret: "nps_...",
onDelayed: "return",
});
const tx = await nylonpay.collectPaymentAndResolve({ /* ... */ });
if (tx.isOk && tx.value.delayed && tx.value.status === "pending") {
// Payment still in flight, listen for webhooks
}Set maxPollDurationMs when you want to bound the total wait time.
Use safeTry from slang-ts to handle the promise without try/catch:
import { safeTry } from "slang-ts";
const result = await safeTry(() => payment.wait());
if (result.isOk) {
console.log("paid:", result.value.reference);
} else {
console.log("failed or timed out:", result.error);
}All operations return Result<T, string> from slang-ts. Use parseError to get structured error objects.
import { parseError } from "@nile-squad/nylonpay-ts";
const result = await nylonpay.getStatus({ reference: "550e8400-e29b-41d4-a716-446655440000" });
if (!result.isOk) {
const error = parseError(result.error);
if (error.retryable) {
// retry
}
}USD, EUR, GBP, KES, UGX, TZS, RWF, CDF
MIT