Skip to main content
Escrow lets your app hold money from one Monei user until a condition is met, then release it to the recipient, on your own schedule and your own confirmation logic. Common uses: marketplace orders, service bookings, milestone-based payments, anything where a buyer and seller need trust before money changes hands.
Escrow is a native Monei feature. It works for users acting directly through the Monei app, and for third-party apps via Monei Connect. This page covers the Connect side.

How it works

Escrow uses a hold model, not a transfer model. When you place a hold, the payer’s funds aren’t moved anywhere, they’re simply marked unavailable to spend. Nothing physically leaves their wallet until you release it.
1

Hold funds

Call escrow:create with a payer, payee, and amount. The payer’s available balance drops by that amount immediately. Their total balance doesn’t change.
2

Payer confirms fulfilment

Your app decides when the condition is met, order delivered, service completed, milestone reached. There’s no built-in timer or webhook for this: you own that logic entirely.
3

Release or refund

Release moves the held amount to the payee and closes out the hold. Refund releases the hold back to the payer without moving any money, since nothing was ever moved in the first place.
This means refunds are instant and free, there’s no reversed transfer to wait on. It also means a held amount never sits in a pooled or intermediary account, it stays attached to the payer’s own wallet the entire time it’s held.

Scopes

ScopeWhat it allowsSensitive
escrow:createHold funds from the user’s walletYes
escrow:releaseRelease held funds to the payeeYes
escrow:refundReturn held funds to the payerYes
escrow:disputeFlag a held escrow as disputedYes
escrow:readView escrow transaction statusNo
Dispute resolution is not exposed via Connect. Once an escrow is disputed, only a Monei admin can resolve it. Your app can flag a dispute, but the outcome is decided on Monei’s side. See Disputes below.

Platform fees

If your app takes a cut on transactions it facilitates, pass platformFeeAmount when you place the hold. The fee is deducted from the payee’s payout at release time, the payer is never charged more than the amount you hold.
Your app needs a fee recipient wallet configured before you can hold funds with a nonzero platformFeeAmount. Contact support@monei.cc to set this up, holds with an unconfigured fee recipient are rejected before any funds are touched.

Endpoints

escrow:create

POST /api/v1/escrow/hold
{
  "payeeId": "f4468d5f-b7dc-446f-80d4-47e3c00b6bcd",
  "amount": 5000,
  "platformFeeAmount": 250,
  "holdReference": "order-8842",
  "metadata": { "orderId": "8842" }
}
holdReference is your idempotency key. Retrying a hold with the same reference returns the original hold instead of creating a second one, safe to retry on timeouts or network errors. platformFeeAmount is optional and only valid alongside a configured fee recipient. Omit it entirely if your app doesn’t take a cut. For milestone-based payments, add depositAmount to release a portion immediately while the rest stays held:
{
  "payeeId": "f4468d5f-b7dc-446f-80d4-47e3c00b6bcd",
  "amount": 50000,
  "depositAmount": 20000,
  "holdReference": "booking-330"
}
The response status will be PARTIAL rather than HELD when a deposit has already gone out.

escrow:release

POST /api/v1/escrow/{id}/release
{}
Releasing with an empty body releases everything still held. To release part of a milestone escrow, pass an amount:
{ "amount": 15000 }
Only the payer can call this endpoint for a given escrow, your app should trigger this after your own confirmation logic decides the payer’s side of the deal is done, not the payee’s.

escrow:refund

POST /api/v1/escrow/{id}/refund
{}
Refunds everything still held back to the payer. Only the payee can call this endpoint, they’re the one giving up their claim to the funds.

escrow:dispute

POST /api/v1/escrow/{id}/dispute
{ "reason": "Order not delivered" }
Either party can flag a dispute. Once disputed, release and refund are both frozen, they’ll return 403 Forbidden until a Monei admin resolves the dispute.

escrow:read

GET /api/v1/escrow/{id}
GET /api/v1/escrow/mine/paying
GET /api/v1/escrow/mine/receiving
GET /api/v1/escrow/mine/pending-incoming
mine/paying and mine/receiving list the user’s escrow history from each side. mine/pending-incoming returns a single figure, the total still held and committed to the user as a payee, useful for showing “funds on the way” before they’re spendable.

Escrow status values

StatusMeaning
HELDFunds are held, nothing released yet
PARTIALSome funds released (e.g. a milestone deposit), remainder still held
DISPUTEDFrozen. release and refund are blocked until an admin resolves it
RELEASEDFully released to the payee
REFUNDEDFully returned to the payer

Disputes

Dispute resolution is intentionally kept off Connect. Your app can raise a dispute with escrow:dispute, but the actual outcome, release or refund, is decided by a Monei admin, not by either party or by your app’s backend. This is by design: allowing either side (or an integrator acting on their behalf) to resolve their own dispute would defeat the point of escrow. If your product needs its own dispute-handling workflow (e.g. a support team reviewing evidence before deciding), build that on your side and contact Monei with the outcome you’ve reached. Automated dispute resolution via Connect may be added in a future version.

Example: order flow

const MONEI_BASE = 'https://api.monei.cc/api/v1';

// Hold funds when the order is placed
const hold = await fetch(`${MONEI_BASE}/escrow/hold`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${accessToken}`,
  },
  body: JSON.stringify({
    payeeId: merchant.moneiUserId,
    amount: order.total,
    platformFeeAmount: order.platformFee,
    holdReference: `order-${order.id}`,
    metadata: { orderId: order.id },
  }),
});

const escrow = await hold.json();

// Later, once the buyer confirms delivery
await fetch(`${MONEI_BASE}/escrow/${escrow.id}/release`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${buyerAccessToken}`,
  },
  body: JSON.stringify({}),
});
import requests

MONEI_BASE = "https://api.monei.cc/api/v1"

# Hold funds when the order is placed
hold_res = requests.post(
    f"{MONEI_BASE}/escrow/hold",
    headers={"Authorization": f"Bearer {access_token}"},
    json={
        "payeeId": merchant_monei_user_id,
        "amount": order_total,
        "platformFeeAmount": order_platform_fee,
        "holdReference": f"order-{order_id}",
        "metadata": {"orderId": order_id},
    },
)

escrow = hold_res.json()

# Later, once the buyer confirms delivery
requests.post(
    f"{MONEI_BASE}/escrow/{escrow['id']}/release",
    headers={"Authorization": f"Bearer {buyer_access_token}"},
    json={},
)
Note the release call uses the buyer’s access token, not the merchant’s, since only the payer can trigger a release.

Scopes Reference

See how escrow scopes fit alongside the rest of Monei Connect

Errors & Rate Limits

What a blocked or unauthorized escrow call looks like