Marketplace escrow works when each seller's share of a payment has its own hold: you can refund one item, release another, and still reconcile the full payment.
Your backend already records one provider charge and splits the checkout into seller-specific orders. Those records tell you who sold each item, its price, and whether it has shipped. They do not show what has happened to each order's money. A delivered order could still be held, ready for payout, awaiting provider confirmation, or already paid.
Take Craftlane. Emma pays $200 in one checkout: $80 for Alice's lamp and $120 for Ben's bookshelf. Before shipping, Alice finds the lamp cracked and cancels her order. Ben later delivers the bookshelf. Craftlane now needs to refund Emma $80 while keeping Ben's $120 on its own path to release and payout.
The practical rule is to track funds at the level that can have its own refund or release decision. Each seller order needs a financial record linked to the original charge, with its held, refundable, payable, and externally settled amounts kept clear as events arrive. We'll use Blnk Core to create a separate hold for each seller order, then follow each one through its refund or release outcome.
Create two holds
Start with the seller orders you already have. Alice's lamp order accounts for $80 of Emma's payment, and Ben's bookshelf order accounts for $120. Since either order can be cancelled or completed on its own, give each one a hold that you can resolve on its own too. Both holds stay linked to the same checkout and provider payment.
Emma still makes one payment, and the provider handles the cash according to Craftlane's payment arrangement. What your backend needs is a financial record for each hold: its amount, the seller order it belongs to, the original payment, and whether it is still held or has been released or cancelled. Then, when a refund or payout needs to happen, your code can find the reservation for that order.
There is also Craftlane's commission to account for. For this example, suppose Craftlane takes 10% when an order completes and earns nothing on cancelled orders. Ben's $120 will eventually become $108 payable to him and $12 in commission. While his order is held, both amounts must wait for the same release decision.
For each hold, you therefore need to record both the full amount reserved for the order and how it will be divided if the order completes. The seller's proceeds and Craftlane's commission depend on that same order, so they should be released or cancelled together. The lamp's hold and the bookshelf's hold still need their own decisions. That is the relationship to preserve in whichever ledger you use.
In Blnk, an inflight transaction is a transfer that stays on hold until you commit or void it. A split inflight transaction lets that hold cover more than one planned transfer. For Ben's order, those transfers are $108 to his payable balance and $12 to Craftlane's commission balance.
Blnk calls each transfer a leg and groups them under a parent ID. Because committing or voiding the parent acts on all its pending legs, we give each seller order its own parent.
For Ben's order, Craftlane reserves the $120 and divides it between his $108 seller proceeds and the $12 commission, keeping both amounts under the same release decision.
The amounts are in cents, so 8000 represents $80. The commission amounts come from Craftlane's 10% policy, but neither commission is earned while its order is still held.
Setting inflight: true reserves the amount until you commit or void it. The atomic flag keeps the seller and commission legs of each order together if one leg fails. It applies within each split, while the two seller orders remain separate.
When your checkout handler sends the request, Blnk accepts it into its queue before it finishes creating the hold. The request and the initial queue response are shown together below.
async function createOrderHold(
sellerOrderId: string,
grossCents: number,
commissionCents: number,
sellerPayableBalanceId: string,
) {
return blnk.Transactions.create({
precise_amount: grossCents,
precision: 100,
currency: 'USD',
source: checkoutClearingBalanceId,
reference: `hold-${checkoutId}-${sellerOrderId}`,
inflight: true,
atomic: true,
destinations: [
{
identifier: sellerPayableBalanceId,
precise_distribution: String(grossCents - commissionCents),
},
{
identifier: platformCommissionBalanceId,
precise_distribution: String(commissionCents),
},
],
meta_data: {
checkout_id: checkoutId,
seller_order_id: sellerOrderId,
provider_payment_id: providerPaymentId,
},
});
}
const aliceHoldRequest = await createOrderHold('lamp-alice', 8000, 800, alicePayableBalanceId);
const benHoldRequest = await createOrderHold('bookshelf-ben', 12000, 1200, benPayableBalanceId); {
"reference": "hold-checkout-emma-lamp-alice",
"status": "QUEUED",
"meta_data": {
"QUEUED_PARENT_TRANSACTION": "txn_0b59f6e6-6c4a-4efa-915c-526f77ef61ab"
}
} The parent ID in meta_data.QUEUED_PARENT_TRANSACTION lets you come back to Alice's hold when her order changes, so save it against the lamp order and do the same for Ben's.
Your checkout handler still needs to confirm that both legs of each order reached INFLIGHT before treating the money as reserved. You can get that confirmation through a webhook or query; the initial queued response is only the acknowledgement of the request.
If the checkout handler runs again after a timeout, you want it to find the lamp's existing allocation rather than set aside another $80. A unique constraint on the checkout ID and seller order ID gives you one allocation record to return to. Keep the transaction reference on that record so you can look up what happened before deciding whether to send the request again. The record stays after the order is cancelled or released, because you will still need to explain where its money went.
Refund the cancelled order
Now Alice reports the cracked lamp. Because the $80 has its own hold, you can cancel the planned transfer to Alice and Craftlane without changing Ben's reservation. Alice receives no seller proceeds, and Craftlane earns no commission on her cancelled order.
When your cancellation handler picks up the lamp order, it needs to mark the allocation for refund so the same amount cannot also be released to Alice. Once that decision is recorded, you can void the order's inflight split using its saved parent ID:
const aliceVoidRequest = await blnk.Transactions.updateStatus(aliceHoldId, {
status: 'void',
}); Here, aliceHoldId is the queued parent saved for the lamp. Once both legs are confirmed void, the $80 reservation is released in the clearing balance, but Craftlane still owes that amount to Emma.
From the clearing balance, move the $80 to a refund-pending balance so it stays assigned to Emma while you arrange the refund. During that move, your allocation code also needs to keep the $80 marked for refund so it cannot be used for another order just because the clearing balance shows it as available.
Emma receives her money through the payment provider, so the refund flow also needs to request $80 back against her original payment. While that request is pending, your ledger still shows that Craftlane owes her $80. When the provider confirms the refund, you record the result against the lamp order and checkout.
If it fails, that obligation stays with Emma's refund while you resolve it. Your integration needs to handle the provider's confirmation and the refund capabilities of the payment method you accepted.
While all of this happens, Ben's $120 stays held under his own parent ID. His order does not need to wait for the lamp's refund to finish before it can be considered for release.
Release the completed order
Ben's delivery gives your payment flow a different decision to make. Suppose Craftlane's rule is to release funds after delivery is confirmed. Once the bookshelf meets that condition, its $120 can leave the held state, with $108 becoming payable to Ben and $12 becoming Craftlane's commission. You can apply that decision by committing the parent saved for his order:
const benCommitRequest = await blnk.Transactions.updateStatus(benHoldId, {
status: 'commit',
}); The saved ID is what keeps this release tied to the bookshelf. Alice's cancelled order belongs to a different parent, so this commit leaves her refund alone. Since the update also goes through the queue, your release handler needs the resulting APPLIED records for both legs before treating the split as complete. You can match those results to the original parent through webhooks or polling.
If you're building the seller payout flow too, this order now contributes $108 to the amount you can pay Ben. Once you send the payout request, that $108 needs to stay reserved for the request so another run cannot select it again.
The provider's result determines what happens next: a confirmed payout records that Ben has been paid, while a confirmed failure makes the amount available for retry. Until you have that result, the ledger release tells you what Craftlane owes Ben, and the payout remains pending.
Account for the full payment
After Emma's refund and Ben's payout succeed, you should be able to trace the original payment to these amounts:
| Part of Emma's payment | Final allocation |
|---|---|
| Refund for Alice's cancelled lamp | $80 |
| Ben's payout for the bookshelf | $108 |
| Craftlane's commission on the bookshelf | $12 |
| Total | $200 |
There is one more amount your payment integration will encounter: the provider's processing fee. Suppose the final fee for this checkout is $6 and Craftlane bears it. That makes it Craftlane's expense, leaving $6 of the commission after the cost. Under this policy, Ben still receives $108 and Emma still receives her full $80 refund.
If the provider deducts that $6 from settlement, the amount arriving from Emma's original payment is $194. Your records can explain the difference as $200 collected, less the $6 expense, while preserving the order allocations. You will also need the provider's settlement and availability information when deciding when to send payouts, because confirmation that Emma paid does not by itself mean the cash is available to pay Ben.
When someone asks what happened to Emma's payment, the shared payment ID lets you follow the whole $200 through the two orders and their outcomes. If Ben asks about his payout, his seller order ID takes you to the $120 sale, the $12 commission, and the $108 payment. The refund and payout records also need their provider references so you can match the ledger's history to what happened outside Craftlane.
If you use Blnk Cloud to investigate that question, the transaction details page gives you the transaction's amount, status, parent, metadata, and inflight history. You can inspect the lamp's void records and the bookshelf's applied records, then check the order and payment IDs that connect them. That gives the person supporting your payment flow a way to inspect the same decisions your code made.
What this means for your payment flow
Whether you're adding partial refunds or building seller payouts, each decision should start from the amount assigned to that order and its current financial state. Craftlane can refund the lamp while the bookshelf stays held, then release Ben's share when his delivery qualifies. The shared payment reference keeps those separate decisions connected to Emma's checkout.
You can apply that model in your own ledger by keeping a hold and its outcome tied to each order. For the Blnk implementation, the marketplace escrow guide covers the underlying hold mechanics, and the split payments guide goes further into allocating seller proceeds and fees.