Transaction Session Callbacks
When you create a payment session with a callbackUrl (e.g. a virtual-account funding session), 100Pay POSTs a callback to that URL when the session ends.
| Event | Trigger |
|---|---|
transaction_session.completed | The session received full payment |
transaction_session.expired | The session timed out before full payment |
Unlike other webhook families, these are sent to the session’s callbackUrl rather than your registered webhook endpoints. If the callbackUrl matches one of your registered endpoints, delivery goes through the standard webhook pipeline (dedupe, retries, verification-token header); otherwise it is a direct POST with no retries — prefer using your registered webhook URL as the callbackUrl.
Payload
Note the event field is named event (not eventType).
interface TransactionSessionCallback {
event: "transaction_session.completed" | "transaction_session.expired";
eventId: string; // UUID
timestamp: string; // ISO 8601
sessionId: string;
status: "completed" | "expired";
channel: string; // e.g. "virtualAccount"
requestedAmount: number;
totalAmountPaid: number;
currency: string;
paymentsCount: number;
// Present on expiry reconciliation:
paymentStatus?: {
isPartial: boolean;
isComplete: boolean;
percentageComplete: number;
};
}Every request also carries an X-Session-Id header with the session ID.
Example — completed
{
"event": "transaction_session.completed",
"eventId": "4a5b6c7d-8e9f-4a1b-9c2d-3e4f5a6b7c8d",
"timestamp": "2026-03-12T14:41:00.000Z",
"sessionId": "VIR_SESSION_1700000000000_XXXXXXXX",
"status": "completed",
"channel": "virtualAccount",
"requestedAmount": 50000,
"totalAmountPaid": 50000,
"currency": "NGN",
"paymentsCount": 1
}Example — expired with partial payment
{
"event": "transaction_session.expired",
"eventId": "5b6c7d8e-9f0a-4b2c-8d3e-4f5a6b7c8d9e",
"timestamp": "2026-03-12T15:10:00.000Z",
"sessionId": "VIR_SESSION_1700000000000_YYYYYYYY",
"status": "expired",
"channel": "virtualAccount",
"requestedAmount": 50000,
"totalAmountPaid": 20000,
"currency": "NGN",
"paymentsCount": 1,
"paymentStatus": {
"isPartial": true,
"isComplete": false,
"percentageComplete": 40
}
}Handling
async function handleSession(event: TransactionSessionCallback) {
if (event.event === "transaction_session.completed") {
await markOrderPaid(event.sessionId, event.totalAmountPaid);
return;
}
// Expired — check for partial payment
if (event.totalAmountPaid > 0) {
await handlePartialPayment(event.sessionId, event.totalAmountPaid, event.requestedAmount);
} else {
await cancelOrder(event.sessionId);
}
}Payments received by a session also emit their own payment charge events. Use the session callback for session lifecycle (paid in full / expired), and the charge events for individual payments.