Escrows
Escrows are automatically created when a buyer pays through a payment link. You don't create escrows directly — they are the result of a successful payment. Escrows protect both buyers and sellers by holding funds until the buyer confirms delivery (goods) or the client confirms the job is complete (services) — see kind on every escrow.
Escrow lifecycle
ACTIVE → AWAITING CONFIRMATION → COMPLETED
↘ DISPUTED ←──────┘ (either party, before completion)
DISPUTED → COMPLETED / REFUNDED
↘ REFUNDED
↘ CANCELLEDAn escrow comes into existence already active— it is created at the moment payment is confirmed, with the funds locked. There is no observable "awaiting payment" escrow state; before payment, only the payment link exists.
Status descriptions
| Status | Description |
|---|---|
active | Payment received, funds locked |
awaiting_confirmation | Seller marked it shipped (goods) or the provider marked the job completed (services) — waiting for confirmation |
disputed | A dispute has been opened, funds locked until resolved |
completed | Confirmed by the buyer/client, funds released to the seller |
refunded | Full refund issued to buyer |
cancelled | Transaction cancelled |
Fee structure
Fees are added on top of the transaction amount at payment time. The buyer pays the base amount plus fees; the seller always receives the full base amount on release.
- Platform fee— Kashia's fee on every transaction (default 1.5%, minimum ₦100, cap ₦50,000)
- Marketplace fee — optional commission for marketplace merchants (0–20%, optional cap). Single-vendor merchants charge 0%.
- Fees are computed and frozen when the payment link is created, and copied onto the escrow at payment — a platform fee change in between does not affect the link
- Example: ₦1,000,000 item + ₦15,000 platform fee + ₦100,000 marketplace fee → buyer pays ₦1,115,000, seller receives ₦1,000,000
- On refund or cancellation, the buyer receives the base amount only — fees are non-refundable. See Fees for details.
Fetching an escrow
Every payment link can carry your own order_id — it flows onto the escrow when the buyer pays, appears in every escrow.*webhook, and is the easiest handle for your system to fetch the escrow with. You never need to store Kashia's ids: look the escrow up by your order_id, or by the payment link's id or reference.
- Pass exactly one of
order_id,payment_link_id, orpayment_link_referenceas a query parameter. - If the payment link exists but hasn't been paid, there is no escrow yet — you get
404with the link's status in the message. - The lookup is scoped to the API key's environment: a test-key lookup never returns a live escrow, and vice versa.
- The response includes
delivery(once the seller has marked the escrow delivered) anddispute(when one exists — its state only; the dispute conversation itself lives on the hosted dispute pages).
/api/v1/external/escrows/lookupResolve an escrow from your order_id or from the payment link, with full details.
Request example
curl "https://vault-api.kashiahq.com/api/v1/external/escrows/lookup?order_id=ORD-12345" \
-H "X-API-Key: your_api_key"Response
{
"success": true,
"data": {
"id": "cb3dc305-2061-4d1b-9062-aec4c33fd849",
"reference": "ESC-42ae9d82",
"order_id": "ORD-12345",
"payment_link_id": "5d2b066d-534e-40f7-aa7f-ccd7a44dd949",
"payment_link_reference": "PL-47886d77",
"title": "iPhone 15 Pro Max",
"amount": 150000000,
"currency": "NGN",
"fees": {
"platform_fee": 2250000,
"merchant_fee": 0,
"total_fees": 2250000,
"total_amount": 152250000
},
"seller_amount": 150000000,
"status": "active",
"environment": "live",
"provider": "monnify",
"payment_method": "bank_transfer",
"buyer": {
"id": "...",
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe"
},
"seller": {
"id": "...",
"email": "ada@example.com",
"first_name": "Adaobi",
"last_name": "Okafor"
},
"delivery": {
"shipped_at": "2025-01-16T09:00:00Z",
"tracking_number": "TRK-999",
"carrier": "GIG Logistics",
"dispatch_name": "Sunday O",
"estimated_delivery_date": "2025-01-20"
},
"dispute": {
"id": "…",
"reference": "DSP-397ef1e3",
"status": "open",
"reason": "item_not_received",
"created_at": "2025-01-17T08:00:00Z"
},
"funded_at": "2025-01-15T10:30:00Z",
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
}Link exists but is unpaid (no escrow yet)
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "resource not found: no escrow yet for order_id \"ORD-12345\" — payment link PL-47886d77 is active; the escrow is created when the buyer pays"
}
}| Parameter | Type | Required | Description |
|---|---|---|---|
order_id | string | No | Your own transaction identifier, set when creating the payment link. |
payment_link_id | string (UUID) | No | The payment link's id. |
payment_link_reference | string | No | The payment link's reference (PL-…). |
/api/v1/external/escrows/:escrowIdRetrieve the full details of one escrow by its id (the same response shape as the lookup). The escrow id arrives in the escrow.created webhook and in a paid payment link's escrow_id field.
Dashboard endpoints
/api/v1/merchant/escrowsList escrows for the authenticated merchant. Requires Merchant JWT from dashboard login.
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | No (default: all) | Filter by escrow status |
page | integer | No (default: 1) | Page number |
per_page | integer | No (default: 20) | Items per page |
/api/v1/merchant/escrows/:idReturns full escrow details including buyer, seller, status history, and delivery. (Dispute state is on GET /external/escrows/lookup and the Disputes endpoints.)
Escrow actions
The /external/… endpoints use your API key and always carry a user_id naming who is acting; the /escrows/… endpoints use dashboard session tokens. Marketplace apps can also use hosted action links — see Action Links.
/api/v1/external/escrows/:escrowId/deliverRecord delivery (goods) or job completion (services) via API key. user_id must be the escrow's seller/provider. On services escrows, shipping fields are refused — pass notes (completion summary) and upload evidence images instead.
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string (UUID) | Yes | The escrow's seller — anyone else gets 403. |
shipping_details | object | No | Optional { tracking_number, carrier, carrier_website, dispatch_name, dispatch_phone, estimated_delivery_date, notes }. |
Request example
// goods escrow
{
"user_id": "seller-uuid",
"shipping_details": { "tracking_number": "TRK-999", "carrier": "GIG Logistics" }
}
// services escrow — completion summary instead of shipping fields
{
"user_id": "provider-uuid",
"shipping_details": { "notes": "Replaced the kitchen tap and re-sealed the sink. Before/after photos attached." }
}/api/v1/external/escrows/:escrowId/delivery/imagesUpload a delivery-proof image (multipart: file, user_id = the seller, optional image_type, default shipping_proof).
/api/v1/external/escrows/:escrowId/confirmConfirm receipt via API key. user_id must be the escrow's buyer.
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string (UUID) | Yes | The escrow's buyer — anyone else gets 403. |
notes | string | No | Optional confirmation notes. |
Request example
{ "user_id": "buyer-uuid" }/api/v1/escrows/:id/deliverMark escrow as shipped. Seller or merchant. Optional shipping_details body.
/api/v1/escrows/:id/confirmConfirm delivery and release funds. Buyer only.
/api/v1/escrows/:id/cancelCancel escrow. Merchant only.
Request example
{ "reason": "..." }/api/v1/escrows/:id/refundIssue a full refund. Merchant or admin.
Request example
{ "reason": "..." }