Payment Events
Event-driven payment status tracking with on, once, and wait
PaymentInstance Events
The PaymentInstance returned by collectPayment() emits events as the payment progresses through its lifecycle.
const payment = await nylonpay.collectPayment({
/* ... */
});
payment.on('processing', handleProcessing);
payment.on('success', handleSuccess);
payment.on('failed', handleFailed);
payment.on('cancelled', handleCancelled);
payment.on('error', handleError);Event Reference
| Event | When It Fires |
|---|---|
processing | Payment accepted and in flight. Fires once. Includes on_hold (payout in review). Read statusText for the review sentence |
success | Payment completed successfully |
failed | Payment failed (customer rejected or timeout) |
cancelled | Payment cancelled by customer or system |
error | Server-side initiation failure (auth, limit, provider, network, timeout), transaction never started |
Offline and Nylon down
Pass onError when creating the SDK client. An unreachable failure has
category: "network" and code: "unreachable". It also reaches the payment
instance "error" event for collection and payout initiation. See Error
Handling.
EventData
Every handler receives an EventData object:
type EventData = {
event: PaymentEvent;
reference: string;
transaction?: Transaction;
error?: string;
reason?: SdkErrorReason;
retryable?: boolean;
timestamp: string;
};event, the event that fired ("processing","success", etc.)reference, the transaction reference, always present, on every eventtransaction, full transaction record. Present on terminal status events (success,failed,cancelled). Theprocessingevent can fire before the full record is fetched; usereferencethereerror, error message. Present on the"error"eventcategory, machine-readable error category. Present on the"error"eventcode, optional Nylon error code.unreachableidentifies offline or Nylon-down callsretryable, whether re-invoking the same operation may succeed. Present on the"error"eventtimestamp, ISO 8601 timestamp of when the event was emitted
payment.on('error', ({ error, category, retryable }) => {
console.error('Payment error:', error, 'category:', category, 'retryable:', retryable);
});on(event, handler)
Registers a handler that fires every time the event occurs.
payment.on('success', ({ transaction }) => {
console.log('Transaction ID:', transaction.id);
});once(event, handler)
Registers a handler that fires only once, then automatically removes itself.
payment.once('success', ({ transaction }) => {
// This runs only on first success
if (transaction.metadata) {
fulfillOrder(transaction.metadata.orderId);
}
});off(event, handler)
Removes a previously registered handler.
const handler = ({ transaction }) => console.log(transaction.status);
payment.on('success', handler);
// ... later
payment.off('success', handler);Chaining
Event methods return the PaymentInstance, enabling chaining.
payment
.on('success', handleSuccess)
.on('failed', handleFailed)
.on('cancelled', handleCancelled)
.on('error', handleError);wait()
Returns a Promise<Transaction | null> that resolves when the payment reaches a terminal state. Resolves with the Transaction on success, or null on failure, cancellation, or lifecycle error. Never rejects.
const tx = await payment.wait();
if (tx) {
// Payment succeeded
console.log('Payment succeeded:', tx.id);
} else {
// Payment failed, cancelled, or error
console.error('Payment did not succeed');
}Status Polling
The PaymentInstance tracks status transitions by polling the backend until the
payment reaches a terminal state; you don't manage the loop yourself. Polling is
single-flight (only one request in flight at a time) and adds a small random
jitter to each interval, so many concurrent payments don't synchronise into a
burst against the status endpoint. Polling stops automatically on any terminal
event, or when the attempt/duration caps are reached.
Tune the cadence and limits with maxPollAttempts and maxPollDurationMs (see
Configuration).
Express Route Handler Example
import express from 'express';
import { createNylonPay } from '@nile-squad/nylonpay-ts';
const app = express();
const nylonPay = createNylonPay({
/* config */
});
app.post('/api/pay', async (req, res) => {
const { amount, phoneNumber, orderId } = req.body;
// References must be valid UUIDs; use the auto-generated one or supply your own
const reference = crypto.randomUUID();
try {
const payment = await nylonPay.collectPayment({
amount,
currency: 'UGX',
customer: {
name: 'Customer Name',
phoneNumber,
},
description: 'Product purchase',
reference,
metadata: { orderId },
});
// Respond immediately, process async
res.json({ reference, status: 'initiated' });
// Handle outcome asynchronously
payment
.on('success', ({ transaction }) => {
updateOrderStatus(orderId, 'paid');
})
.on('failed', () => {
updateOrderStatus(orderId, 'payment_failed');
})
.on('error', (data) => {
console.error('Payment error:', data.error);
});
} catch (err) {
// Initiation error, check err.category
res.status(400).json({ error: err.category, message: err.message });
}
});When to Use Events vs wait()
| Scenario | Approach |
|---|---|
| HTTP API with immediate response | Use on() handlers, respond 202 Accepted |
| Background job/processor | Use on() for state updates |
| CLI or script or simple flow | Use await payment.wait() |
| Need specific state only | Use once() |
Transaction Payload
type Transaction = {
id: string;
reference: string;
amount: number;
currency: Currency;
status: TransactionStatus;
type: TransactionType;
method: PaymentMethod;
description: string;
operatorTid?: string | null;
phone: string;
email: string | null;
failureReason: string | null;
failureCategory?: FailureCategory | null;
failureCode?: FailureCode | null;
statusText?: string;
delayed?: boolean;
metadata: Record<string, string>;
mode: TransactionMode;
createdAt: string;
updatedAt: string;
};
type TransactionStatus =
| 'pending'
| 'processing'
| 'on_hold'
| 'successful'
| 'failed'
| 'cancelled';