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
   ↘ CANCELLED

An 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

StatusDescription
activePayment received, funds locked
awaiting_confirmationSeller marked it shipped (goods) or the provider marked the job completed (services) — waiting for confirmation
disputedA dispute has been opened, funds locked until resolved
completedConfirmed by the buyer/client, funds released to the seller
refundedFull refund issued to buyer
cancelledTransaction 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, or payment_link_reference as 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) and dispute (when one exists — its state only; the dispute conversation itself lives on the hosted dispute pages).
GET/api/v1/external/escrows/lookup

Resolve an escrow from your order_id or from the payment link, with full details.

Request example

bash
curl "https://vault-api.kashiahq.com/api/v1/external/escrows/lookup?order_id=ORD-12345" \
  -H "X-API-Key: your_api_key"

Response

json
{
  "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)

json
{
  "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"
  }
}
Parameters
ParameterTypeRequiredDescription
order_idstringNoYour own transaction identifier, set when creating the payment link.
payment_link_idstring (UUID)NoThe payment link's id.
payment_link_referencestringNoThe payment link's reference (PL-…).
GET/api/v1/external/escrows/:escrowId

Retrieve 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

GET/api/v1/merchant/escrows

List escrows for the authenticated merchant. Requires Merchant JWT from dashboard login.

Parameters
ParameterTypeRequiredDescription
statusstringNo (default: all)Filter by escrow status
pageintegerNo (default: 1)Page number
per_pageintegerNo (default: 20)Items per page
GET/api/v1/merchant/escrows/:id

Returns 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.

POST/api/v1/external/escrows/:escrowId/deliver

Record 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.

Parameters
ParameterTypeRequiredDescription
user_idstring (UUID)YesThe escrow's seller — anyone else gets 403.
shipping_detailsobjectNoOptional { tracking_number, carrier, carrier_website, dispatch_name, dispatch_phone, estimated_delivery_date, notes }.

Request example

json
// 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." }
}
POST/api/v1/external/escrows/:escrowId/delivery/images

Upload a delivery-proof image (multipart: file, user_id = the seller, optional image_type, default shipping_proof).

POST/api/v1/external/escrows/:escrowId/confirm

Confirm receipt via API key. user_id must be the escrow's buyer.

Parameters
ParameterTypeRequiredDescription
user_idstring (UUID)YesThe escrow's buyer — anyone else gets 403.
notesstringNoOptional confirmation notes.

Request example

json
{ "user_id": "buyer-uuid" }
POST/api/v1/escrows/:id/deliver

Mark escrow as shipped. Seller or merchant. Optional shipping_details body.

POST/api/v1/escrows/:id/confirm

Confirm delivery and release funds. Buyer only.

POST/api/v1/escrows/:id/cancel

Cancel escrow. Merchant only.

Request example

json
{ "reason": "..." }
POST/api/v1/escrows/:id/refund

Issue a full refund. Merchant or admin.

Request example

json
{ "reason": "..." }