Escrow API

This page documents the live public escrow surface served under https://api.gopiaxis.com/api.

Overview

Public escrow routes:

  • POST /api/escrows/

  • GET /api/escrows/{escrow_id}

  • GET /api/escrows/{escrow_id}/status

  • POST /api/escrows/{escrow_id}/terms/{term_id}/fulfill

  • POST /api/escrows/{escrow_id}/release

  • POST /api/escrows/{escrow_id}/reverse

  • POST /api/escrows/{escrow_id}/disputes

Public Contract Notes

The guaranteed create-escrow request schema is:

  • receiver_id (optional; defaults to the authenticated merchant account)

  • amount

  • currency_code

  • payment_method

  • terms

  • external_user_id

  • user_info

  • user_location

This differs intentionally from direct payments:

  • direct payment uses currency and recipient_id

  • escrow uses currency_code and receiver_id

  • direct payment credits the authenticated merchant collector by default

  • public escrow collection also defaults the release destination to the authenticated merchant

  • if receiver_id is supplied, it must match the authenticated merchant Account.id

  • receiver_id must be a Piaxis Account.id; do not send a MerchantProfile.id or UserProfile.id

Some long-form examples may include richer descriptive keys inside term payloads. That is acceptable as term-specific data, but the guaranteed outer request contract is the field list above.

Authentication

Merchant-controlled calls use:

 api-key: YOUR_MERCHANT_API_KEY
 Content-Type: application/json
X-piaxis-Client-ID: YOUR_MERCHANT_CLIENT_ID

For piaxis_external sender flows, use OAuth bearer auth where the route requires user-context authorization.

For unregistered external-money flows, request an OTP first with POST /api/request-otp and carry it in user_info.otp.

Create Escrow

POST /api/escrows/

Create a new escrow.

Canonical MTN escrow example

 POST /api/escrows/ HTTP/1.1
 Host: api.gopiaxis.com
 api-key: YOUR_API_KEY
 Content-Type: application/json
X-piaxis-Client-ID: YOUR_MERCHANT_CLIENT_ID

 {
   "amount": "50000.00",
   "currency_code": "UGX",
   "payment_method": "mtn",
   "terms": [
     {
       "type": "delivery_confirmation",
       "data": {
         "deadline": "2026-12-31T23:59:59Z"
       }
     }
   ],
   "user_info": {
     "email": "[email protected]",
     "phone_number": "+256700000000",
     "otp": "123456"
   }
 }

Receiver semantics

Public escrow collection infers the release destination from the authenticated merchant API key.

  • omitting receiver_id uses the authenticated merchant Account.id automatically

  • if receiver_id is supplied, it must still match the authenticated merchant Account.id

  • this public collection route does not use receiver_id to choose some other merchant or platform account

  • if you want to hold funds through escrow and later pay an internal or external beneficiary, release into the authenticated merchant wallet first and then create a disbursement

Marketplace sequence

For a platform-managed order or service flow:

  1. authenticate as the merchant or platform account that should collect and create the escrow

  2. the customer pays into escrow

  3. release the escrow to that same authenticated merchant or platform account when the order or service is complete

  4. if the final beneficiary should be paid on MTN or Airtel mobile money, create a separate direct disbursement to the beneficiary phone number

This keeps the roles explicit:

  • the authenticated merchant receives the escrow release on this public collection route

  • recipients[].recipient_id is only for later internal wallet payouts

  • external mobile-money payees are handled later through the disbursement routes with phone_number

Choose the right flow

Use direct payment when the authenticated merchant should collect immediately and no later escrow fulfillment is needed.

Use public escrow collection when the authenticated merchant should still be the final collector, but release should wait for payer and merchant actions such as delivery confirmation or meeting verification.

Use direct disbursement when you already control the collected balance and want to pay a beneficiary immediately.

Use escrow disbursement when the payout beneficiary, not the collecting merchant, must satisfy terms before payout. In escrow-disbursement batches the merchant or platform is the escrow sender and the beneficiary becomes the escrow receiver.

If your rule is collect from buyer B now, pay user C later, keep the collection on POST /api/escrows/ with the merchant as receiver, release into the merchant wallet, and then create a direct disbursement or escrow disbursement for user C.

Canonical piaxis_external escrow example

POST /api/escrows/ HTTP/1.1
Host: api.gopiaxis.com
api-key: YOUR_API_KEY
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json

{
  "amount": "75000.00",
  "currency_code": "UGX",
  "payment_method": "piaxis_external",
  "external_user_id": "platform_user_123",
  "terms": [
    {
      "type": "delivery_confirmation",
      "data": {
        "deadline": "2026-12-31T23:59:59Z"
      }
    }
  ]
}

Guaranteed create response shape

{
  "id": "7680cfa9-b0cf-43a7-974b-108b89bd5ebe",
  "amount": "50000.00",
  "currency_code": "UGX",
  "status": "pending",
  "payment_method": "mtn",
  "sender_id": null,
  "receiver_id": "096b723a-45c5-4957-94d7-747835136265",
  "created_at": "2026-01-15T10:30:00Z",
  "updated_at": "2026-01-15T10:30:00Z",
  "terms": [],
  "products": [],
  "location": null,
  "required_actions": [],
  "external_user_id": null
}

Read Escrow

GET /api/escrows/{escrow_id}

Fetch the escrow record in the canonical typed response shape.

GET /api/escrows/{escrow_id}/status

Fetch an operational status view with buyer info, term statuses, and all_terms_met.

The two routes are both public and intentionally return different detail shapes.

Fulfill Terms

POST /api/escrows/{escrow_id}/terms/{term_id}/fulfill

Fulfill a single escrow term.

Role model on this public collection route

For escrows created by POST /api/escrows/ the protected actors are:

  • payer or customer = escrow sender

  • authenticated merchant collector = escrow receiver

That means:

  • buyer-side confirmations must come from the original payer

  • seller-side confirmations must come from the authenticated merchant whose account matches receiver_id

  • if you want a downstream seller, driver, or beneficiary to become the protected receiver instead, use POST /api/escrow-disbursements rather than trying to route collection escrow directly to that user

Inspect required actions first

Use GET /api/escrows/{escrow_id} and inspect required_actions before building a fulfill UI.

Typical action hints returned by the live code include:

  • delivery_confirmation_required with confirmation_method

  • delivery_meeting_required with max_distance_km

  • agreement_required with agreement_options and current status

  • proof_required with proof_type and optional deadline

Term-specific payloads

The outer request shape stays the same, but data keys vary by term_type. The live handler branches on those keys, so send the term-specific payload instead of a generic confirmed flag.

Delivery confirmation

Send confirmation_method plus either buyer_device_info or seller_device_info.

Buyer confirmation example:

{
  "term_id": "4215567e-6928-4a9d-85d8-c3b1708bdcec",
  "term_type": "delivery_confirmation",
  "data": {
    "confirmation_method": "signature",
    "buyer_device_info": {
      "device_id": "buyer-device-001",
      "platform": "android",
      "app_version": "1.0.0"
    }
  },
  "user_info": {
    "email": "[email protected]",
    "phone_number": "+256700000000",
    "otp": "123456"
  }
}

Seller confirmation example:

{
  "term_id": "4215567e-6928-4a9d-85d8-c3b1708bdcec",
  "term_type": "delivery_confirmation",
  "data": {
    "confirmation_method": "signature",
    "seller_device_info": {
      "device_id": "merchant-ops-01",
      "platform": "web"
    }
  }
}

Use user_info for an unregistered external payer. Use bearer auth for registered piaxis or piaxis_external payers. Use the authenticated merchant API key for the seller-side confirmation.

Meeting delivery

Buyer confirmation sends buyer_latitude and buyer_longitude. Seller confirmation sends seller_latitude and seller_longitude. Both sides may include device_info. The route records caller IP automatically and only completes the term after both sides submit coordinates within the allowed proximity.

Buyer meeting example:

{
  "term_id": "4215567e-6928-4a9d-85d8-c3b1708bdcec",
  "term_type": "meeting_delivery",
  "data": {
    "buyer_latitude": 0.3476,
    "buyer_longitude": 32.5825,
    "device_info": {
      "device_id": "buyer-phone-01",
      "platform": "android"
    }
  },
  "user_info": {
    "email": "[email protected]",
    "phone_number": "+256700000000",
    "otp": "123456"
  }
}

Seller meeting example:

{
  "term_id": "4215567e-6928-4a9d-85d8-c3b1708bdcec",
  "term_type": "meeting_delivery",
  "data": {
    "seller_latitude": 0.3477,
    "seller_longitude": 32.5826,
    "device_info": {
      "platform": "web"
    }
  }
}

Other supported term types

  • agreement: send agreement_type as release, reverse, or split; when using split, also send split_percentage. This flow currently requires a registered account actor.

  • location: send latitude and longitude.

  • meeting: send latitude and longitude.

  • password: send password.

  • proof: submit files first; later send verify as true after the proof is ready to approve.

  • custom: send the term-specific data expected by your custom rule.

  • return_period: send return_requested when the buyer is requesting a return within the allowed window.

Guaranteed request fields:

  • term_id

  • term_type

  • data

  • user_info

Delivery confirmation and meeting-delivery responses include requires_other_party until both sides have submitted their part. Other term types may return pending or release the funds immediately once all terms are met.

Release Escrow

POST /api/escrows/{escrow_id}/release

Guaranteed request fields:

  • verification_code

  • verification_method

  • user_info

  • force

  • reason

Example

{
  "force": true,
  "reason": "Buyer requested immediate release",
  "user_info": {
    "email": "[email protected]",
    "phone_number": "+256700000000",
    "otp": "123456"
  }
}

Reverse Escrow

POST /api/escrows/{escrow_id}/reverse

Guaranteed request fields:

  • reason

  • verification_code

  • verification_method

  • user_info

Disputes

POST /api/escrows/{escrow_id}/disputes

Guaranteed request fields:

  • reason

  • initiator_role

  • user_info

Implementation Notes

  • Persist the returned escrow_id immediately.

  • Reconcile state from both polling and webhooks.

  • Model term fulfillment as idempotent on your side.

  • Authenticate collections as the merchant or platform account that should collect released funds.

  • Use disbursement recipients, not receiver_id, when you need to route money to a different internal or external beneficiary later.