Statuses & Lifecycle
Every money object carries four state machines.
Every money object carries four statuses. They move independently, and none of them implies any of the others. Don't infer the value of one status from another.
What kind of transaction it is
Every money object carries a transaction_type, and three subtype fields of which only the matching one is populated. The other two are null.
transaction_type | Subtype field | Values |
|---|---|---|
PAY_IN | pay_in_subtype | COLLECTION, ON_RAMP, USD_STABLE_DEPOSIT |
PAY_OUT | pay_out_subtype | OFF_RAMP, FIAT_WITHDRAWAL, CRYPTO_WITHDRAWAL, CROSS_BORDER_PAYOUT |
CONVERSION | conversion_subtype | OTC_TRADE, BALANCE_CONVERSION |
Branch on the values you handle and fall through to a default for anything else, the same way you would with an event type. A subtype you do not recognize should not stop you reading the statuses, which is where the outcome lives.
1. transaction_status
This is the instruction's own lifecycle, and it is the status you surface to your end user.
CREATED · PENDING_VALIDATION · PENDING_FUNDS · PENDING_COMPLIANCE · PENDING_RFI · APPROVED_FOR_EXECUTION · PROCESSING · COMPLETE · FAILED · CANCELLED · BLOCKED
Everything starts at CREATED, and a successful transaction ends at COMPLETE.
Once a transaction reaches COMPLETE, FAILED, CANCELLED or BLOCKED it is terminal. Nothing reaches back into it after that.
2. compliance_status
This says whether the controls have cleared.
NOT_REQUIRED · PENDING_SCREENING · PENDING_KYT · CLEARED · ALERTED · RFI_REQUESTED · PAUSED · ESCALATED · RELEASED · BLOCKED
It is independent of the business lifecycle by design. A transaction can be PROCESSING with compliance CLEARED, or it can be PENDING_COMPLIANCE with compliance ALERTED.
PENDING_KYT is specific to inbound crypto. Every inbound crypto leg is screened, and a hit holds the leg. Alfred resolves the hit and moves the leg to RELEASED, ESCALATED or BLOCKED. You are never asked to do anything, so you only need to reflect the state you are given.
3. settlement_status
This says whether the funds actually landed. It sits at the leg level and describes the movement of money.
NOT_STARTED · FUNDS_EXPECTED · FUNDS_RECEIVED · FUNDS_HELD · FUNDS_RELEASED · SETTLED · PARTIALLY_SETTLED · FAILED · RETURNED
This is why a delayed settlement is not a business status. A delayed leg shows up as a settlement value while the parent stays PROCESSING.
Each leg also carries leg_status, with these values:
CREATED · SUBMITTED_TO_PROVIDER · ACCEPTED_BY_PROVIDER · PROCESSING · COMPLETED · FAILED · RETURNED · REVERSED
That value is the current one only. The history lives in the leg's status events rather than in an inline array on the leg.
4. reconciliation_summary
ALL_MATCHED · PARTIAL_MATCH · HAS_BREAK
This one is read-only and rolled up from the legs. It never blocks anything.
What the split buys you
An example shows why the model works this way.
A payout that's owed reads PROCESSING with FUNDS_EXPECTED and CLEARED. The money left Alfred and delivery is pending. Nothing is under investigation and there's nothing for you to do.
A payout on a compliance hold reads PENDING_COMPLIANCE with ALERTED and settlement untouched. Nothing has moved and the outcome is undecided.
With a single status field those two cases look the same, and you would need a reason code to tell them apart. Four separate statuses distinguish them without one.
RFIs have two scopes
An RFI is a request for information with an owner and a clock. The state machine is the same wherever it is raised, but what it blocks depends on the scope.
| Scope | Attached to | Blocks |
|---|---|---|
| Onboarding | The customer | Approval. Customer sits at UNDER_REVIEW or AWAITING_*. |
| Transaction | One transaction | That movement only. Parent reads PENDING_RFI with compliance_status: RFI_REQUESTED. |
OPEN · WAITING_ON_CUSTOMER · WAITING_ON_PARTNER · RECEIVED · RESOLVED · OVERDUE · REJECTED
An approved customer never regresses because of a transaction RFI. The transaction waits and the customer stays ACTIVE. That is why the two scopes are kept separate.
Read rfi_owner before chasing anyone. Under a reliance model the answer is often owed by you instead of by your end customer. An OVERDUE RFI blocks the transaction.
Screening hits resolve on compliance_status rather than on a separate field. A transaction on a compliance hold reads PENDING_COMPLIANCE with ALERTED and settlement untouched, and Alfred resolves it. Nothing is asked of you.
Customer status is separate
Customer status sits outside all four statuses above. It is worth reading because it tells you who has to act next.
NOT_STARTED · INCOMPLETE · AWAITING_UBO · AWAITING_QUESTIONNAIRE · UNDER_REVIEW · ACTIVE · PAUSED · REJECTED · OFFBOARDED
AWAITING_UBO and AWAITING_QUESTIONNAIRE are the most useful values. They name what is outstanding, so you can prompt your end customer for the specific thing you need instead of telling them the review is still pending.
Reading all four
GET /v1/payouts/fiat/{payoutId}
GET /v1/deposits/{depositId}
GET /v1/conversions/{conversionId}
GET /v1/transactions/{transactionId}/legs
The legs read is where reconciliation actually happens. A cross-currency payout has two legs, LP_EXECUTION and FIAT_RAIL. Each leg has its own provider, its own provider reference and its own statuses. Without the legs read you would have one reference for a movement that had two, and matching against a provider statement would be guesswork.
Updated 5 days ago

