Mono Colombia

Bank transfer lifecycle

State machine for bank transfers and batches — transitions, webhooks, and terminal states.

A bank transfer is a payout: money leaves your Mono account and lands in a beneficiary's Colombian bank account over one of three rails — ACH, Transfiya, or Mono Turbo. Every transfer you create through POST /transfers is automatically grouped into a batch, and both the transfer and the batch move through their own state machines in parallel. This page explains what each state means, what causes each transition, and which webhook event fires at each step.

Understanding both state machines matters because your webhook handler will receive batch events (was the batch authorized? sent?) and transfer events (was the individual transfer approved or rejected?) in an interleaved stream.

Transfer state machine

Transfer states at a glance

StateTerminal?Webhook event
createdNo— (synchronous API response)
in_progressNobank_transfer_fallback_routing fires if a routing retry occurs
approvedYesbank_transfer_approved
declinedYesbank_transfer_rejected
cancelledYes— (user-initiated before dispatch; no async event)
duplicatedYes— (surfaced via batch_duplicated at the batch level)

Final states can change

The bank_transfer_change_final_state event fires in the rare case where a transfer already in approved or declined is moved to a different final state by the rail. Always handle this event idempotently — update your local record to the new state even if you already considered it settled.

Batch state machine

Every call to POST /transfers creates a batch, even when you submit a single transfer. The batch is the unit that goes through admin authorization (OTP) if your account requires it.

Batch states at a glance

StateTerminal?Webhook event
createdNo— (synchronous API response)
pending_otpNobatch_authorization_requested
verified_otpNo— (internal; transitions immediately to processing_transactions)
processing_transactionsNobatch_sent
approvedYes— (individual transfers emit bank_transfer_approved)
partially_approvedYes— (mix of bank_transfer_approved and bank_transfer_rejected per transfer)
declinedYes— (individual transfers emit bank_transfer_rejected)
duplicatedYesbatch_duplicated
canceledYesbatch_canceled

Detailed walkthrough

Transfer: created

Initial state. The transfer row exists in the system with an id you can use for lookups. Mono has accepted the request but has not yet submitted it to the rail. Every transfer in the batch enters this state together.

What happens next: once the batch clears any OTP gate and reaches processing_transactions, each transfer moves to in_progress.

Transfer: in_progress

The transfer has been dispatched to the selected rail — ACH, Mono Turbo, or Transfiya. Funds are in transit; the source account balance has been debited but the destination has not yet confirmed credit.

If the primary rail rejects the attempt and you set a fallback_routing, Mono retries automatically. This fires bank_transfer_fallback_routing without changing the transfer state — the transfer stays in_progress through the retry.

Next states:

  • approved when the rail confirms the credit.
  • declined when the rail rejects and no fallback succeeds.

Transfer: approved (terminal)

The destination bank has confirmed the credit. Funds are in the beneficiary's account. The bank_transfer_approved webhook fires with the final routing and amount. No further transitions occur under normal circumstances.

Transfer: declined (terminal)

The transfer was rejected. The declination_reason field on the webhook payload carries the rejection reason (for example, insufficient_funds or invalid_account_information). The bank_transfer_rejected webhook fires. No further transitions occur under normal circumstances.

Transfer: cancelled (terminal)

The transfer was canceled by a user before the batch reached processing_transactions. Canceling a batch cancels all its transfers. No async webhook fires for individual transfers — the batch emits batch_canceled.

Transfer: duplicated (terminal)

The entity_id on this transfer was already used in a previous batch. The transfer is not processed. The batch emits batch_duplicated.


Batch: created

The batch row exists in the system. All its transfers are in created. This state is synchronous — you get the batch id in the API response.

What happens next: Mono checks whether the batch requires OTP authorization.

Batch: pending_otp

The batch requires explicit authorization from an Administrator user before it can be processed. The batch_authorization_requested webhook fires. No transfers move until an admin authorizes.

Next states:

  • verified_otp when an admin completes authorization.
  • canceled when an admin cancels.

Batch: verified_otp

The admin has authorized. Mono prepares the batch for dispatch. This state is transient — it moves to processing_transactions within seconds and no webhook fires for verified_otp itself.

Batch: processing_transactions

The batch is being dispatched. Each transfer moves to in_progress. The batch_sent webhook fires. From this point, individual transfer webhooks (bank_transfer_approved / bank_transfer_rejected) drive reconciliation.

Batch: approved (terminal)

All transfers in the batch were approved. Individual bank_transfer_approved events have already fired for each transfer.

Batch: partially_approved (terminal)

Some transfers were approved and some were declined. Each transfer has already emitted its own bank_transfer_approved or bank_transfer_rejected event. Reconcile transfer by transfer using the individual events.

Batch: declined (terminal)

All transfers in the batch were declined. Individual bank_transfer_rejected events have already fired.

Batch: duplicated (terminal)

Every transfer in the batch had an entity_id that already existed. No transfers are processed. batch_duplicated fires. Individual transfers move to duplicated.

Batch: canceled (terminal)

An admin canceled the batch before it reached processing_transactions. All transfers move to cancelled. batch_canceled fires.

Working with these state machines in your integration

  1. Key off webhooks, not polling. Every meaningful transition emits a webhook. Subscribe to both transfer and batch events — they carry different information and arrive in interleaved order.

  2. Handle out-of-order delivery. Webhook deliveries are at-least-once and may arrive out of order. If you receive bank_transfer_approved before batch_sent, keep the later state and ignore the earlier one. A safe rule: if a transfer is already in a terminal state locally, ignore non-terminal webhooks for it.

  3. Build for the OTP gate. If your Mono account requires batch authorization, your batch will sit in pending_otp until an admin acts. Plan your UX around this: expose the batch state to operators so they know when action is required.

  4. Distinguish transfer state from batch state. A batch in partially_approved does not tell you which transfers succeeded — you must inspect individual transfer events. Reconcile transfer by transfer, not batch by batch.

  5. Handle bank_transfer_change_final_state. It is rare, but a transfer already in approved or declined can move to a different final state. Update your local record when this event arrives; do not treat a terminal state as immutable until you stop receiving events for that transfer.

  6. Use entity_id for idempotency. Submitting the same entity_id twice results in duplicated. This is your safety net against double-submission — but it also means you must generate a fresh entity_id for every genuinely new transfer.

Next steps

On this page