Process rail pay-in and pay-out events
Handle approved rail pay-in, pay-out, and quote-consumed events without exposing rail or settlement internals.
Rail events are for approved flows that surface rail pay-in or pay-out status. They should update your product state and support records, but they should not expose provider, treasury, reserve, or settlement implementation details to users.
Rail events are approval-gated
Document and handle rail events only for partner accounts and products that are approved for those event families. Keep provider names and settlement internals out of public docs and customer-facing UI.
Event families
| Event family | Meaning | Recommended action |
|---|---|---|
rail.pay_in.processing | A pay-in has started processing. | Mark local record as processing. |
rail.pay_in.settled | A pay-in settled successfully. | Close the pay-in and reconcile value once. |
rail.pay_in.failed | A pay-in failed. | Close or retry based on product policy. |
rail.pay_in.cancelled | A pay-in was cancelled. | Stop downstream fulfillment. |
rail.pay_out.processing | A pay-out has started processing. | Mark local record as processing. |
rail.pay_out.settled | A pay-out settled successfully. | Close the pay-out and notify the user. |
rail.pay_out.failed | A pay-out failed. | Move to support or retry. |
rail.pay_out.cancelled | A pay-out was cancelled. | Stop downstream completion. |
quote.consumed | A signed quote was redeemed. | Attach quote consumption to the related attempt. |
Processing flow
Worker pattern
async function processRailEvent(event: ObitWebhookEvent) {
const railRecord = await railRecords.findByExternalReference(event.data.id);
if (!railRecord) {
await supportQueue.enqueue({ reason: 'unknown_rail_reference', eventId: event.id });
return;
}
const inserted = await eventLog.insertOnce({ eventId: event.id, type: event.type });
if (!inserted) return;
if (event.type.endsWith('.settled')) {
await railRecords.settleOnce({ railRecordId: railRecord.id, eventId: event.id });
} else if (event.type.endsWith('.failed') || event.type.endsWith('.cancelled')) {
await railRecords.closeOnce({ railRecordId: railRecord.id, eventId: event.id, status: 'closed' });
} else {
await railRecords.markProcessing({ railRecordId: railRecord.id, eventId: event.id });
}
}Public-safe UI copy
| Internal condition | User-facing copy |
|---|---|
| Processing | "Your transfer is processing." |
| Settled | "Your transfer is complete." |
| Failed | "This transfer could not be completed. Please try again or contact support." |
| Cancelled | "This transfer was cancelled." |
| Unknown or delayed | "We are checking the latest status." |