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

  • external_order_id

  • allocations

  • metadata

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.

Marketplace Allocations

For marketplace orders where the buyer pays once but the merchant remains the only escrow receiver, create one escrow to the merchant and represent internal seller, fulfillment, or service slices with allocations.

This is the recommended pattern when:

  • the merchant is the contractual counterparty and must remain accountable for settlement

  • downstream sellers are not individually onboarded as direct escrow receivers

  • the payer should only be charged once for the order

Do not create one payer-funded escrow per downstream seller for the same order when the merchant remains the sole escrow receiver. Use one merchant-held escrow instead.

Allocation-aware create requests additionally support:

  • external_order_id for merchant-side order reconciliation

  • allocations for per-slice amounts and references; totals must sum to the escrow amount

  • metadata for merchant-defined JSON that should remain attached to the escrow

Allocation-aware responses additionally include:

  • balance_escrow_in and balance_escrow_out for remaining releasable and reversible balances

  • persisted metadata

  • normalized allocations

  • allocation_summary with pending, released, and reversed counts and amounts

Merchants should keep these allocation balances and partial action controls visible in the same order-management surface where the user placed the order.

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",

Allocation-aware marketplace create example

{
  "amount": "50000.00",
  "currency_code": "UGX",
  "payment_method": "mtn",
  "external_order_id": "ORDER-789",
  "allocations": [
    {
      "allocation_key": "seller-alpha",
      "amount": "20000.00",
      "seller_reference": "seller-001",
      "description": "Alpha seller settlement"
    },
    {
      "allocation_key": "seller-beta",
      "amount": "30000.00",
      "seller_reference": "seller-002",
      "description": "Beta seller settlement"
    }
  ],
  "metadata": {
    "channel": "marketplace"
  },
  "terms": [],
  "user_info": {
    "email": "[email protected]",
    "phone_number": "+256700000000",
    "otp": "123456"
  }
}
      "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
}

Allocation-aware marketplace create example

{
  "amount": "50000.00",
  "currency_code": "UGX",
  "payment_method": "mtn",
  "external_order_id": "ORDER-789",
  "allocations": [
    {
      "allocation_key": "seller-alpha",
      "amount": "20000.00",
      "seller_reference": "seller-001",
      "description": "Alpha seller settlement"
    },
    {
      "allocation_key": "seller-beta",
      "amount": "30000.00",
      "seller_reference": "seller-002",
      "description": "Beta seller settlement"
    }
  ],
  "metadata": {
    "channel": "marketplace"
  },
  "terms": [],
  "user_info": {
    "email": "[email protected]",
    "phone_number": "+256700000000",
    "otp": "123456"
  }
}

When allocations are present, create and read responses may also include:

  • balance_escrow_in

  • balance_escrow_out

  • metadata

  • allocations

  • allocation_summary

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

  • amount

  • allocation_keys

Example

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

Allocation-aware partial release example

{
  "reason": "Merchant confirmed seller alpha items",
  "allocation_keys": ["seller-alpha"],
  "amount": "20000.00",
  "user_info": {
    "email": "[email protected]",
    "phone_number": "+256700000000",
    "otp": "123456"
  }
}

Allocation-aware partial release response

{
  "status": "released",
  "escrow_id": "43452aa2-28b4-4e8f-a1d2-d47014c9a33a",
  "amount": "20000.00",
  "force": false,
  "reason": "Merchant confirmed seller alpha items",
  "allocation_keys": ["seller-alpha"],
  "allocation_summary": {
    "total_count": 2,
    "pending_count": 1,
    "released_count": 1,
    "reversed_count": 0,
    "pending_amount": "30000.00",
    "released_amount": "20000.00",
    "reversed_amount": "0.00"
  }
}

Short-lived approval session for nearby partial releases

For external payer escrows created with mtn, airtel, or card where the payer is still an unregistered external user, the first successful release request that includes user_info.email, user_info.phone_number, and user_info.otp opens a short-lived approval session scoped to that escrow.

During that approval window:

  • subsequent nearby partial releases on the same escrow may omit user_info.otp

  • user_info.email and user_info.phone_number must still be sent and must still match the original payer

  • if the approval window expires, send a fresh OTP again before the next release

Example follow-up partial release without a fresh OTP

{
  "reason": "Merchant confirmed seller beta items",
  "allocation_keys": ["seller-beta"],
  "amount": "30000.00",
  "user_info": {
    "email": "[email protected]",
    "phone_number": "+256700000000"
  }
}

Reverse Escrow

amount and allocation_keys are also supported here for allocation-aware partial reversals on marketplace escrows.

POST /api/escrows/{escrow_id}/reverse

Guaranteed request fields:

  • reason

  • verification_code

  • verification_method

  • user_info

  • amount

  • allocation_keys

Allocation-aware partial reverse example

{
  "reason": "Seller beta items were cancelled",
  "allocation_keys": ["seller-beta"],
  "amount": "30000.00",
  "user_info": {
    "email": "[email protected]",
    "phone_number": "+256700000000",
    "otp": "123456"
  }
}

Allocation-aware partial reverse response

{
  "status": "reversed",
  "escrow_id": "43452aa2-28b4-4e8f-a1d2-d47014c9a33a",
  "amount": "30000.00",
  "reason": "Seller beta items were cancelled",
  "allocation_keys": ["seller-beta"],
  "allocation_summary": {
    "total_count": 2,
    "pending_count": 0,
    "released_count": 1,
    "reversed_count": 1,
    "pending_amount": "0.00",
    "released_amount": "20000.00",
    "reversed_amount": "30000.00"
  }
}

Allocation-Aware Partial Reverse:

POST /api/escrows/43452aa2-28b4-4e8f-a1d2-d47014c9a33a/reverse HTTP/1.1
Host: api.gopiaxis.com
api-key: YOUR_API_KEY
Content-Type: application/json

{
  "reason": "Seller beta items were cancelled",
  "allocation_keys": ["seller-beta"],
  "amount": "30000.00",
  "user_info": {
    "email": "[email protected]",
    "phone_number": "+256700000000",
    "otp": "123456"
  }
}

Allocation-Aware Partial Reverse Response:

{
  "status": "reversed",
  "escrow_id": "43452aa2-28b4-4e8f-a1d2-d47014c9a33a",
  "amount": "30000.00",
  "reason": "Seller beta items were cancelled",
  "allocation_keys": ["seller-beta"],
  "allocation_summary": {
    "total_count": 2,
    "pending_count": 0,
    "released_count": 1,
    "reversed_count": 1,
    "pending_amount": "0.00",
    "released_amount": "20000.00",
    "reversed_amount": "30000.00"
  }
}

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.

  • Keep marketplace allocation balances and partial release or reverse actions visible on the order page where the user placed the order.

  • Persist external_order_id and stable allocation_key values for reconciliation across your OMS, support tooling, and settlement reports.

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