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 (awaiting customer confirmation). Fires once, right after initiation — even when the payment resolves before the first status poll |
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 |
EventData
Every handler receives an EventData object:
type EventData = {
event: PaymentEvent;
reference: string;
transaction?: Transaction;
error?: string;
category?: SdkErrorCategory;
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"eventretryable, 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;
metadata: Record<string, string>;
mode: TransactionMode;
createdAt: string;
updatedAt: string;
};
type TransactionStatus =
| 'pending'
| 'processing'
| 'successful'
| 'failed'
| 'cancelled';