Managed Transactions¶
A Managed Transaction is a transaction initiated by the MAJORITY platform itself. This contrasts with Network Transactions, which originate from external networks (e.g., the VISA network). A typical example of a managed transaction is a remittance transaction initiated by the remittance service.
Backend services can create managed transactions by calling the ITransactionService interface in Transactions.
Managed Transaction Flow¶
flowchart LR
BEService[Backend Service]
TransactionCompletedEvent(TransactionCompletedEvent)
BEService -- 1: optional risk check --> Risk
BEService -- 2: synchronous call --> Transactions
Transactions -. 3: publishes .-> TransactionCompletedEvent
TransactionCompletedEvent -. 4: receives .-> BEService
- The Backend Service performs a risk check, depending on business rules.
- Call
ITransactionServicein Transactions. See the details for the different kinds of transactions below. - Transactions will coordinate the creation/update of the transaction in the underlying Ledger, depending on the method called.
- The Backend Service will receive a
TransactionCompletedEventas a final confirmation of the transaction. Note that reserved transactions can receive multipleTransactionCompletedEvents if the transaction is released.
Transaction Creation Guidelines¶
Creating Transactions¶
- Always generate a new
TransactionId(random GUID) and pass it when creating transactions throughITransactionService. Do NOT reuse GUIDs from external providers. - The
TransactionIdacts as the idempotency key. A second call with the sameTransactionIdwill not create a new transaction; it will return the latest state (including current balances if applicable). - By default, transactions happen on the user's default Account. Pass a different
MajorityAccountIdto target another account. - Choosing the correct
TransactionType(ManagedDebitTransactionType/ManagedCreditTransactionType) andTransactionSubTypeis critical. These drive product logic and how funds actually move. Coordinate with Product before introducing new (sub)types and when deciding what type to use. This document outlines the many considerations around types. - Always subscribe to the
TransactionCompletedEvent. This event is the source of truth for the final outcome (success, reversal, expiration, etc.). See Failures below. - Remember to consider if a push notification should be sent to he user and if so at what time and what content it should have.
Failures¶
A call can fail with a known domain error (e.g., InsufficientFundsException, AccountBlockedException). These indicate the transaction was not created or applied.
For unexpected errors, the transaction state is unknown: the operation may have succeeded or failed. You may safely retry using the SAME TransactionId to avoid duplicates.
Callers MUST subscribe to TransactionCompletedEvent to confirm final status - even if the initial call returned success. Example: a Reservation may later expire automatically.
Forced Transactions¶
A transaction can be forced by setting ForceTransaction/IsForced in the call. A forced transaction will skip the checks for balance and account status; it will always be applied (as long as the account exists).
Forcing transactions is useful when a transaction should skip validation. This often happens when a transaction has already gone through in an external system, or because the balance/account rules should be disregarded for a specific transaction. Some examples:
- A notification from a 3rd party that a cash deposit did not go through, and money should not be deposited.
- A transaction is disputed and the user must receive provisional credits.
Force can be used for these kinds of transactions:
CreditFunds&DebitFundsReverseFundsPayFundsScheduleDebitFunds
OwnerResourceId¶
OwnerResourceId is a caller-chosen identifier that can group several related transactions (e.g., a single business process). It can be seen as a caller-generated TransactionFlowId. Its usefulness is reduced when you already rely on TransactionId for idempotency, but it is still important for reservations as it identifies the original reservation transaction and the original transaction in reversals.
Notifications & Title¶
The initiator is responsible for if a push notification should be sent out, when it should be sent and what content it should have.
Push notifications are created by publishing the SendPushNotificationRequestMessage message.
Note: For managed transactions implemented before 2025-11 the Transaction service was responsible for the push notifications. These should ideally be moved to initiator at some point. The migration is tracked by the "Push Notification" column in the Ownership checklist
Kinds of Managed Transactions¶
Credit & Debit¶
Credit and Debit transactions move money directly into or out of the account. The backend treats them as Posted immediately, impacting both Available Balance and Posted Balance at once.
Methods:
CreditFunds– Increase funds on the account.DebitFunds– Decrease funds on the account.
Emulated¶
An Emulated transaction is a lightweight "display-only" transaction. It is written only to the transaction list and:
- Does NOT affect any balance.
- Does NOT appear in the Ledger.
Used to show something the user expects to see even though no real transfer occurred (e.g., a $0 subscription fee during trial).
Methods:
EmulateCreditFunds– Display-only credit analogue.EmulateDebitFunds– Display-only debit analogue.
Reservations¶
Money can be reserved before being moved. Reserving funds decreases the Available Balance but does not yet post to the Posted Balance. All reservations have a lifetime. If not explicitly debited or released before expiry, they are automatically released.
To debit a reservation or release the reservation, you need to pass in both the OwnerResourceId of the original reservation and a TransactionId. The OwnerResourceId will be used to identify the transaction to debit/release, and the TransactionId will be used as the id for the new debit/release transaction.
If no lifetime is specified in ReserveFunds, a default of 30 days applies.
Subscribe to TransactionCompletedEvent to detect automatic expiration and other updates to the transaction.
Methods:
ReserveFunds- Reserve the fundsDebitReservedFunds- Debit the funds (e.g. the outcome will be same asDebitFunds)ReleaseReservedFunds- Release the funds.
Scheduled¶
A Scheduled transaction represents a future transaction. It is visible to the user (through Firebase) but:
- Does NOT affect Available or Posted balances yet.
- Is NOT written to the Ledger until completion.
When completed it is transformed into a normal Credit or Debit transaction.
Note that while these transactions are named "Scheduled", they do not have a specific time when the transaction will happen, or a time when they will expire. It is therefore important to not leave any scheduled transactions hanging forever.
Methods:
ScheduleCreditFunds- Schedule a credit in the futureScheduleDebitFunds- Schedule a debit in the futureCancelScheduledFunds- Cancel a scheduled transactionCompleteScheduledFunds- Complete the transaction (e.g. transform it into normal Credit/Debit transaction)
Additional Methods¶
Pay Funds¶
Moves money atomically between two Accounts on the SAME underlying Bank and Lender. Prefer over a Reserve → Debit → Credit sequence when the underlying ledger can process a single transfer (e.g., crypto networks: one on-chain tx → one gas fee and lower latency). Calling PayFunds will still create two Transactions, one on the sender account and one on the receiver account.
Only supported when both accounts share the same bank and ledger.
Methods:
PayFunds- Move money from A to B.
Reversal¶
It is possible to reverse a transaction (logical inverse of the referenced transaction). If too much time has elapsed (30-90 days), a new compensating flow may be created instead.
To reverse a transaction you need to pass in both the OwnerResourceId of the original transaction, and a ReversalTransactionId. The OwnerResourceId will be used to identify the transaction to reverse, and the ReversalTransactionId will be used as the id for the new reverse transaction.
Methods:
ReverseFunds - Reverse the previous transaction
Validation¶
Performs validation only; does NOT execute the transaction. The transaction methods will perform the same validation. Therefore, for the vast majority of operations, you should not call Validate before calling the actual transaction method. For user-initiated transactions, the Apps will check the balance locally on the device, and an additional validation on the backend side does not serve any purpose.
Checks include:
- Account is active.
- Balance rules (e.g., debit will not overdraw an account that disallows negative balances).
Even if the result is positive, the real transaction can still fail later (race conditions, state changes, unexpected errors).
Methods:
Validate - Validate a Managed Transaction.
ValidateNetworkTransactions - Validate a Network Transaction (this is more advanced and does Risk checks)
Deprecated¶
Methods:
FinalizeFundsGivenOnCredit - Deprecated / Not used. Should be removed.
Transaction Details¶
The transaction area and firebase only keep generic transaction details. Type specific details (i.e. a remittance timeline or store details for a cash deposit) is the responsibility of the caller. Each area should, if needed, provide a details endpoint that the App can call for these additional details.
If possible, the specifc transaction details endpoint should be at v1/transactions/details/{transactionId}.
The Apps implmement the logic to decide if additional details is required, and knows the specific endpoint to call.