Payment Events
React to payment outcomes with one typed listener, whichever provider or flow produced them.
Every operation that changes a payment emits an event: a 3D Secure callback, a PayTR notification, a refund made from your admin panel. Listen to the events instead of checking each result, and update your orders in one place.
payment.on('payment.succeeded', async (event) => {
await orders.markPaid(event.conversationId, event.paymentId);
});
payment.on('refund.succeeded', async (event) => {
await orders.addRefund(event.paymentId, event.amount);
});
// Every event
const off = payment.on('*', (event) => console.log(event.type, event.provider));
off(); // removes the listenerEvents are emitted for every provider, and for calls made on the payment object, on a provider, and through the HTTP handler.
Events
| Event | Emitted when |
|---|---|
payment.succeeded | createPayment, completeThreeDSPayment or capture succeeds |
payment.authorized | authorize succeeds: the amount is blocked on the card |
payment.pending | createPayment, completeThreeDSPayment, authorize or capture returns pending, for example with NETWORK_ERROR |
payment.failed | a payment operation, initThreeDSPayment or initThreeDSAuthorize fails |
payment.cancelled | cancel or voidAuthorization succeeds |
refund.succeeded | refund succeeds |
refund.failed | refund fails |
A 3D Secure pre-authorization is completed with completeThreeDSPayment, so it emits payment.succeeded when the amount is blocked. Status queries (getPayment) and card operations emit no events.
Events are emitted only for results the provider verified. A forged or tampered callback (errorCode: 'INVALID_HASH') emits nothing.
The event
interface PaymentEvent {
type: PaymentEventType; // 'payment.succeeded', ...
provider: string; // the provider id, e.g. 'iyzico'
operation: PaymentOperation; // 'createPayment', 'completeThreeDSPayment', ...
paymentId?: string; // the provider's payment id, when known
conversationId?: string; // your order id, when known
amount?: string; // charged or refunded amount, from the request
currency?: string;
code?: PaymentErrorCode; // normalized error code of failures
result: unknown; // the result returned to the caller
request: unknown; // the request (the callback data for 3D Secure completions)
}The listener of a specific event gets the narrowed type: in payment.on('payment.failed', (event) => ...), event.type is 'payment.failed'.
amount and currency come from the request. The callback of a 3D Secure payment carries no amount, so they are missing there: look the order up by conversationId.
When a listener fails
Listeners run in order, after the operation, and are awaited. If one throws, the operation throws an EventListenerError. The payment itself went through, so the error carries the result:
import { EventListenerError } from 'better-payment';
try {
await payment.createPayment(request);
} catch (error) {
if (error instanceof EventListenerError) {
error.result; // the payment result
error.event; // the event whose listener failed
error.cause; // what the listener threw
}
}The HTTP handler handles it for you:
- Provider callbacks (
payment/complete-3ds,callback) answer500. The provider re-sends the callback (PayTR retries its notification), and the listeners run again with the stored result, without calling the provider again. Events are emitted right beforeonCallback, so both are retried together. - Other routes (
payment,refund, ...) answer with the result as usual, and the error is logged with yourlogger. The payment happened: an error response could make the client pay twice.
Write listeners so that running them twice is harmless, for example by checking the order's state before updating it.
Events in plugins
A plugin declares its listeners in events. For each event, the listeners of that event run first, then the * listeners. In both groups, plugin listeners run in the order of the plugins, before the listeners added with payment.on():
import { definePlugin } from 'better-payment';
export const slackAlerts = (webhookUrl: string) =>
definePlugin({
id: 'slack-alerts',
events: {
'payment.failed': async (event) => {
await fetch(webhookUrl, {
method: 'POST',
body: JSON.stringify({ text: `Payment failed on ${event.provider}: ${event.code}` }),
});
},
},
});Duplicate deliveries
Providers can send the same callback more than once. The HTTP handler deduplicates callbacks, so a repeated delivery emits no new event. When you call completeThreeDSPayment() yourself, deduplicate in your listener.