Collecting into a Balance
The order of calls for taking local currency into a customer's balance.
This is a common flow. It covers the order to make the calls in, and each step links to the documentation that explains it in more detail.
Before you start
Three things have to be true, and they are three separate waits rather than one.
- The customer is
ACTIVE. See Onboarding a customer. - The customer holds
fiat_collectionand it isAPPROVED, on both gates. See Endorsements. - A virtual account exists and
virtual_account.activatedhas fired. See Virtual accounts and deposit addresses.
Check the route is LIVE before any of this, because a country and currency that is not live is refused at registration rather than at deposit. See Countries and routes.
Choose the account purpose deliberately. BALANCE holds the money as a local stablecoin. CONVERSION converts every deposit that ever lands there. The value is fixed at generation, so a wrong choice means a new instrument.
The sequence
1. Announce the deposit, if you can.
POST /v1/customers/{customerId}/deposit_intents
This creates the PAY_IN immediately, at PENDING_FUNDS with settlement_status: FUNDS_EXPECTED. The transaction exists before the money does.
Set reference. It is what the payer quotes and the only reliable way to match their money to the customer you expect. Set expectedAmount and tolerance if you want a short or long payment handled by policy rather than by a person.
tolerance takes underPct and overPct separately, so you can accept a small shortfall without accepting a large overpayment.
onUnder applies on a PAYMENT-purpose account. That is where the intent is a locked order, so a shortfall has to be resolved against a price. On a BALANCE or CONVERSION account the intent is an announcement and the money that arrives is what you get.
onUnder | What happens |
|---|---|
resize | The locked rate is kept and the quantity shrinks |
requote | Re-prices at today's rate |
block | Holds until the shortfall is made up or the intent expires |
refund | Returns the receipt |
onOver takes convert_all, hold_surplus or refund_surplus.
A payment inside tolerance fires deposit_intent.partially_matched, which consumes the intent and completes the same way a full match does.
The response carries an indicativeRate with binding: false. It is for display. This endpoint accepts no quoteId and a pay-in never consumes a quote.
You can skip this step. Money still arrives and is still attributed. You lose the reference match and the expected amount, which is what turns an arrival into an order you were waiting for.
2. Give the payer the instructions and the reference.
They send over the local rail.
3. Wait. In order, and each one means something different.
| Event | What it means |
|---|---|
deposit.created | Money arrived and we have a deposit. Nothing is spendable. |
deposit_intent.matched | We attributed it to the intent you announced. Still nothing is spendable. |
deposit.updated | A status moved. Read all four. |
deposit.credited | The balance moved. Now it is spendable. |
Also handle deposit_intent.partially_matched and deposit_intent.expired.
4. Read the balance.
GET /v1/customers/{customerId}/balances
Check the figure landed in available. A total that includes held or reserved money will mislead you. See Balances.
What you learn about the payer
You do not register a payer and there is nothing to submit about them in advance. What the rail tells us arrives on the deposit, in sender, after the money does.
| Field | What it holds |
|---|---|
name | The payer's name, where the rail record carries it |
bank_name | The payer's bank |
account_last4 | The last four digits of the account they paid from |
rail | The rail the payment arrived on |
rail_reference | The rail's own tracking reference |
How much of this is populated depends on the rail. Each market's record carries different fields, so treat sender.name as present where the rail supplies it rather than as guaranteed. rail_reference is the one your customer, their payer and their bank can all see, so it is worth storing.
The payer's tax ID isn't on sender. The rail records carry it in Mexico (RFC or CURP) and Argentina (CUIT), but the API has no field for it today. Don't build a flow that depends on reading it.
Match on reference first. It is the only field a payer quotes deliberately, so it is the most reliable key. Amount and timestamp come next, with sender as supporting evidence. Two invoices for the same figure on the same day will reconcile to the wrong one if you match on amount alone.
match on the deposit records how we attributed it, including match_method and candidate_count. It is persisted at decision time and never recomputed, so it is the audit trail for why this money went to this customer.
The mistake to avoid
A match is not a credit. deposit_intent.matched says we worked out whose money this is and consumed the intent. It does not say the funds settled, and it does not credit anything.
If you release goods, mark an invoice paid, or trigger a payout on the match, you are acting on money that has not landed. Alfred still has to validate the payer against the rail record, and the balance is credited only once that validation is approved. Wait for deposit.credited.
In sandbox
Nothing is provisioned and you cannot transfer funds in, so you simulate the deposit.
POST /v1/test/customers/{customerId}/deposits/simulate
It runs the same pipeline a real deposit runs, so the events above arrive in the same order. See Testing in sandbox.
What comes next
The balance is the customer's. Converting it or paying it out are separate transactions with their own lifecycles, and this one completing starts neither of them. If they belong to one commercial flow, pass the same payment_flow_id to each.
Updated 3 days ago

