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_typeSubtype fieldValues
PAY_INpay_in_subtypeCOLLECTION, ON_RAMP, USD_STABLE_DEPOSIT
PAY_OUTpay_out_subtypeOFF_RAMP, FIAT_WITHDRAWAL, CRYPTO_WITHDRAWAL, CROSS_BORDER_PAYOUT
CONVERSIONconversion_subtypeOTC_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.

ScopeAttached toBlocks
OnboardingThe customerApproval. Customer sits at UNDER_REVIEW or AWAITING_*.
TransactionOne transactionThat 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.


Did this page help you?